Migration d’un forum PhpBB vers une plateforme SharePoint 2010. / Migrate PHPBB forum to SharePoint 2010.

Technologies : PhpBB, SharePoint 2010, base de données MySQL, LinQ to Entity/MySQL, API SharePoint, application console C#, Visual Studio 2010.
Cet article a valeur de tutoriel a pour but d’effectuer la migration d’un forum PhpBB basé sur des technologies gratuites, vers un site SharePoint 2010 de type blog.
Dans le cadre d’un projet réalisable en entreprise ce projet vous fera découvrir les possibilités en matière de développement qu’offre le C# appliqué à SharePoint 2010 et ceci via une application console.
En effet SharePoint 2010 offre un point de partage des connaissances au sein de l’entreprise et la réutilisation des données présentes dans l’entreprise avant l’implémentation de cette plateforme à une forte valeur ajouté dans les arguments de vente de cet outil.
Une forte majorité des projets de développement autour de la plateforme SharePoint 2010 porte en effet sur des problématiques de migration de l’existant.
Prérequis technique :
·         C#
·         Base en LinQ to Entity.
·         Connaissance de l’architecture logique d’un site SharePoint.

Mise en place de l’environnement

Outils nécessaires :
1.       Machine avec SharePoint 2010.
2.       Wamp Server.
3.       PhpBB.
4.       Visual Studio 2010 ou 2008.

Structure de la base de données MySQL

Schéma relationnel de la portion de base de données utilisé.











Génération des classes C# pour LinQ to MySQL

Dans le cadre de l’utilisation d’une base de données MySQL il est nécessaire de générer les classes correspondantes aux tables de la base de données. Pour ce faire j’ai utilisé le logiciel DbLinq dont vous trouverez l’archive en fin d’article.
Voici la commande à exécuter après avoir décompressé le dossier
DbMetal.exe -provider=MySql -database:phpbb -server:localhost
-user:root –password:password -namespace:MigrationPhpBB              
-code:PhpBB.cs –sprocs
En remplaçant les arguments, server, user, password et namespace par les valeurs correspondantes à votre serveur. 

Figure 1. Génération des classes à partir de la base de données MySQL
Un fichier .cs sera alors généré dans le dossier de DbLinq avec pour nom PhpBB.cs

Création du projet Visual Studio

Vous êtes maintenant prêt à développer l’application.  Crée un projet de type Application console depuis les templates proposés par Visual Studio et nommez le  MigrationPhpBB.
Ajoutez le fichier qui a été créé dans le dossier de DbLinq à votre solution (PhpBB.cs). Ainsi qu’un référence vers les .dll  DbLinq.dll et DbLinq.MySql.dll contenu dans le dossier DbLinq. 
Figure 2. Création du projet application console.

Vos premiers échanges avec MySQL

Dans la Main méthode crée une instance de la classe PhPbB.

PhPbB entity = new PhPbB(new MySqlConnection(
                "Server=localhost; Database=phpbb; Uid=root; Pwd=password"));


Pour récupérer la liste de tous les forums contenus dans votre site PhpBB utilisez la ligne de code suivant :

List<PhPbB3Forums> list_forums = (from forums in entity.PhPbB3Forums
                                         select forums).ToList();

Dans mon exemple je n’importe seulement qu’une partie des forums contenus dans le site PhpBB en stockant le nom des forums dans une liste.

List<string> forumAImporter = new List<string>() { "Forum1" ,"Forum2", "Forum3" };
List<PhPbB3Forums> list_categories = (from categories_filtres in
                                       ((from categories_toutes in entity.PhPbB3Forums
                                         select categories_toutes).ToList())
                                   where categories_cible.Contains(categories_filtres.ForumName)).ToList();

Ma list_categories contiendra mes 3 forums et me permettra de faire un parcourt de ceux-ci pour récupérer les différents postes et réponses.

Création des sous-sites SharePoint

Je vais donc dans un premier temps créer un sous site SharePoint pour chacune de mes catégories.
Créez une classe nommé SiteHelper.cs qui contiendra le code suivant :

namespace MigrationPhpBB
{
    static class SiteHelper
    {
        public static string url = "http://formation";
    }
}


Et une classe SharepointHelper.cs qui se chargera de créer les sous-sites, les catégories ainsi que les billets et les réponses.

Avec une première fonction  pour créer le sous-site SharePoint :

/// <summary>
/// Création d'un site de typle Blog
/// </summary>
/// <param name="name">Nom du site</param>
/// <returns>Site crée</returns>
 public static SPWeb CreateBlog(string name)
 {
     using (SPSite site = new SPSite(SiteHelper.url))
     {
         return EnsureWeb(site, name, SPWebTemplate.WebTemplate.Blog, true, true);
      }
 }

/// <summary>
/// Site SharePoint sur lequel créer le blog.
/// </summary>
/// <param name="site">Site SharePoint</param>
/// <param name="webName">Nom du blog</param>
/// <param name="onQuickLaunch">Spécifie si le site aura un lien de lancement rapide</param>
/// <returns>Site crée</returns>
public static SPWeb EnsureWeb(SPSite site, string webName, bool onQuickLaunch)
        {
            SPWeb web = null;
            Guid webID = Guid.Empty;
            if (site != null)
            {
                try
                {
                    //parcous les sous-sites.
                    foreach (SPWeb currentWeb in site.AllWebs)
                    {
                        //verifie si le site existe, si il existe il faut le supprimer.
                        if (currentWeb.Name.ToLower() == webName.ToLower())
                        {
                            Console.WriteLine("Blog " + webName + " existe, suppression");
                            currentWeb.Delete();
                            break;
                        }
                    }
                    #region Creation du blog
                    if (web == null)
                    {
                        //Création d'un site de type Blog.
                        SPWebTemplate webTemplate = site.GetWebTemplates(1036)["BLOG#0"];
                        site.AllowUnsafeUpdates = true;
                        web = site.AllWebs.Add(webName, webName, "Blog " + webName, 1036, webTemplate, false, false);

                        //Supprime les billets crée par défaut.
                        SPList billets = web.Lists["Billets"];
                        foreach (SPListItem it in billets.Items)
                            billets.Items.DeleteItemById(it.ID);

                        //Supprime les catégories crée par défaut.
                        SPList categories = web.Lists["Catégories"];
                        foreach (SPListItem it in categories.Items)
                                categories.Items.DeleteItemById(it.ID);

                        web.Navigation.UseShared = (onQuickLaunch) ? true : false;
                        //Ajout des liens de navigations.
                        if (web.Navigation.UseShared)
                        {
                            //Ajoute un lien vers le forum dans la barre de navigation
                            Microsoft.SharePoint.Navigation.SPNavigationNode pNode = new Microsoft.SharePoint.Navigation.SPNavigationNode(web.ParentWeb.Title, web.ParentWeb.Url.Replace(web.ParentWeb.Site.Url, string.Empty));
                            Microsoft.SharePoint.Navigation.SPNavigationNode cNode = new Microsoft.SharePoint.Navigation.SPNavigationNode("Blog " + web.Title, web.Url);
                            web.ParentWeb.Navigation.TopNavigationBar.AddAsLast(cNode);

                            // add to quick launch
                            Microsoft.SharePoint.Navigation.SPNavigationNodeCollection quickLaunch = web.ParentWeb.Navigation.QuickLaunch;
                            foreach (Microsoft.SharePoint.Navigation.SPNavigationNode node in quickLaunch)
                            {
                                //Ajoute le quick launch dans les sites.
                                if (node.Title == "Sites")
                                {
                                    node.Children.AddAsLast(cNode);
                                    break;
                                }
                            }
                        }
                    }
                    #endregion
                    //Mets à jour le site.
                    web.Update();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    Console.ReadLine();
                }
            }
            else
            {
                throw new Exception("In EnsureSiteDataList SPWeb is null");
            }
            return web;
        }

De retour dans Program.cs il faut créer un sous-site par forum.

foreach (var categorie in list_categories)
{
    #region Création du sous-site
    categorieID = categorie.ForumID;
    categorieName = categorie.ForumName;
    Console.WriteLine();
    Console.WriteLine("--------------------------------------");
    Console.WriteLine("Création du sous site " + categorieName);
    }
    SPWeb blog = SharepointHelper.CreateBlog(categorieName);
}


Figure 3. Création des sous-sites via l'application.

Création des catégories pour les blogs SharePoint

Dans PhpBB les catégories de forum contiennent des forums, qui seront traduit par des catégories dans le blog SharePoint. Il faut donc utiliser la propriété de navigation ParentID de la classe PhpbB3Forums pour retrouver les forums appartenant à une catégorie de forum.

List<PhPbB3Forums> forums = (from fo in entity.PhPbB3Forums
                              where fo.ParentID == categorieID
                              select fo).ToList();
uint forumID;
string forumName;
foreach (PhPbB3Forums forum in forums)
{
     forumID = forum.ForumID;
     forumName = forum.ForumName;
     SPFieldLookupValue lookUpCategorie = SharepointHelper.CreateCategorie(blog,forumName);
     Console.WriteLine("Catégorie crée : " + forumName);
}

Dans la classe SharepointHelper créer la fonction

/// <summary>
/// Création de catégorie pour le site de type blog sharepoint
/// </summary>
/// <param name="blog">Blog sur lequel crée une catégorie</param>
/// <param name="categorie_name">Nom de la catégorie</param>
/// <returns>Lookup de la catégorie</returns>
public static SPFieldLookupValue CreateCategorie(SPWeb blog,string categorie_name)
{
    SPList categories = blog.Lists["Catégories"];
    //Supprime la catégorie si elle existe déjà.
    foreach (SPListItem it in categories.Items)
    if(it.Title == categorie_name)
        categories.Items.DeleteItemById(it.ID);
    SPListItem item;
      item = categories.Items.Add();
      item["Title"] = SiteHelper.Utf8_decode(categorie_name,false);
      item.Update();
      return new SPFieldLookupValue(item.ID, item.Title);
}

L’objet SPFieldLookupValue permet de remplir les propriétés de type LookUp qui fera le lien entre un billet et le forum auquel il appartient.

Crée également la fonction Utf8_decode dans la classe SiteHelper.

public static string Utf8_decode(string s,bool estBillet)
{
   byte[] l = Encoding.Default.GetBytes(s);
   string st = Encoding.UTF8.GetString(l);
   string deco = SPEncode.HtmlDecode(st);
   return deco;
}

Figure 4. Création des catégories depuis l'application

Création des billets et des réponses

A la suite de l’instruction permettant de créer les categories, nous allons parcourir chaque forum PhpBB pour récupérer les topics appartenant à la catégorie précédemment crée.
SPFieldLookupValue lookUpCategorie = SharepointHelper.CreateCategorie(blog,forumName);
Console.WriteLine("Catégorie crée : " + forumName);
#region Création des billets
var topics = (from top in entity.PhPbB3Topics
    where top.ForumID == forumID
    orderby top.TopicLastPostTime
    select top).ToList();

Le corps du topic est contenu dans le premier post lié à ce topic, il faut donc parcourir la table PhpbB3Posts dans le but de récupérer le corps du topic ainsi que les réponses apportés par les utilisateurs.

On va ici utiliser l’adresse email de la personne inscrite sur le forum PhpBB pour relier avec le compte SharePoint en utilisant la propriété Email de l’objet SPUser.
Ajouté cette fonction à la classe SiteHelper.cs
/// <summary>
/// Retourne l'utilisateur grâce à son email.
/// </summary>
/// <param name="web">Site concerné</param>
/// <param name="email">Email de l'utilisateur</param>
/// <returns>Look up Utilisateur</returns>
public static SPFieldLookupValue lookUpAuthor(SPWeb web, string email)
{
    SPFieldLookupValue Lookup = null;
    using (SPSite site = new SPSite(SiteHelper.url))
    {
        try
        {
            foreach (SPUser user in web.AllUsers)
                if(email.ToLower() == user.Email.ToLower())
                    Lookup = new SPFieldLookupValue(user.ID, user.Name);
        }
        catch
        {
             return null;
        }
     }
     return Lookup;
}

Il faut ensuite parcourir la liste des topics ainsi que la liste des posts pour ajouter les billets et les commentaires dans le blog SharePoint.
PhPbB3Users user_topic;
DateTime created;
SPFieldLookupValue lookUpUser=null;
SPFieldLookupValue lookUpBillet = new SPFieldLookupValue();
string user_topic_mail;
foreach (PhPbB3Topics top in topics)
{
       //Le corps du billet se trouve dans le premier commentaire posté, il faut donc aller le récupérer.
       SPFieldLookupValue lookUpCommentaire = null;
       var posts = (from post in entity.PhPbB3Posts
                where post.TopicID == top.TopicID
                orderby post.PostTime
                select post).ToList();
                       
       for(int i = 0; i < posts.Count;i++)
       {
            PhPbB3Posts post = new PhPbB3Posts();
            post = posts[i];
            created = new DateTime(1970, 01, 01).AddSeconds(post.PostTime);
            user_topic = (from us in entity.PhPbB3Users
                        where us.UserID == post.PosterID
                        select us).First();
             user_topic_mail = user_topic.UserEmail;
             correction_mail = "";
             dictionnaire_correction_email.TryGetValue(user_topic_mail, out correction_mail);

             lookUpUser = SiteHelper.lookUpAuthor(blog, user_topic_mail);
             if (i == 0)
             { // Le premier post contient le corps du topic
 // Créer le billet avec le corps de message contenu dans le post.
                  lookUpBillet = SharepointHelper.CreateBillet(top.TopicTitle, post.PostText, created, blog, lookUpCategorie, lookUpUser);
             }
             else
             {
                  lookUpCommentaire = SharepointHelper.CreateCommentaire(lookUpCategorie, post.PostSubject, post.PostText, created, blog, lookUpUser, lookUpBillet);
             }
      }

Il faut maintenant ajouter les fonctions CreateBillet et CreateCommentaire dans la classe SharepointHelper.
/// <summary>
/// Crée un billet.
/// </summary>
/// <param name="title">Titre du billet</param>
/// <param name="body">Corps du billet</param>
/// <param name="created">Date de création du billet</param>
/// <param name="blog">Site sur lequel créer le billet</param>
/// <param name="lookUpCategorie">Catégorie à laquel le billet appartient</param>
/// <param name="lookUpUser">Utilisateur ayant crée le billet</param>
/// <returns>Look up vers le billet</returns>
public static SPFieldLookupValue CreateBillet(
   string title, string body, DateTime created, SPWeb blog, SPFieldLookupValue lookUpCategorie, SPFieldLookupValue lookUpUser
{                         
    SPList billets = blog.Lists["Billets"];
    SPListItem item = billets.Items.Add();
    item["Title"] = SiteHelper.Utf8_decode(title,false);
    if(body.Contains("SMILIES_PATH"))
        body = UploadSmiley(body,blog);

    item["Body"] = SiteHelper.Utf8_decode(body, true);

    if(lookUpCategorie != null)
        item["PostCategory"] = lookUpCategorie;
    if(lookUpUser != null)
        item["Author"] = lookUpUser;
    item["Created"] = created;
    item["PublishedDate"] = created;
    item["_ModerationStatus"] = (int)SPFieldModStat.ListItemMenuState.Allowed;
    item.Update();
    return new SPFieldLookupValue(item.ID, item.Title);
}

public static SPFieldLookupValue CreateCommentaire(
   string titre, string corps, DateTime created, SPWeb blog, SPFieldLookupValue lookUpUser, SPFieldLookupValue lookUpPost)
{
    SPList commentaires = blog.Lists["Commentaires"];
    SPListItem addComm = commentaires.Items.Add();
    if (corps.Contains("SMILIES_PATH"))
       corps = UploadSmiley(corps, blog);
    addComm["Title"] = SiteHelper.Utf8_decode(titre,false);
    addComm["Body"] = SiteHelper.Utf8_decode(corps,false);
    addComm["Created"] = created;
    if(lookUpUser != null)
       addComm["Author"]= lookUpUser;
    if (lookUpPost != null)
    {
         addComm["PostTitle"] = new SPFieldLookupValue(lookUpPost.LookupId, SiteHelper.Utf8_decode(titre,false));
         var tit = addComm["PostTitle"];
    }           
    addComm["_ModerationStatus"] = (int)SPFieldModStat.ListItemMenuState.Allowed;
    addComm.Update();

    return new SPFieldLookupValue(addComm.ID, addComm.Title);
}

Figure 5. Création du billet et du commentaire via l'application.

Attacher les pièces jointes aux messages.

Les pièces jointes  sont de deux types, des images ou des archives de documents. Dans les deux cas il faut dans un premier temps ajouter les fichiers à une bibliothèque de documents présente sur le site et ensuite lier ce fichier au message.
Pour des raisons de facilité j’ai directement copié le dossier contenant les pièces jointes trouvé sur le server PhpBB sur le serveur SharePoint.
Ajoutez dans la classe SiteHelper :
Public static string urlPiecesJointes = "C:/Users/spadm/Desktop/importPhpbb/piecesJointes/";
Etant le chemin vers votre dossier de pièces jointes.
En base de données MySQL dans la table PhpBB3Posts la présence d’une pièce jointe est signalé par le champ  PostAttachment, ajoutez ces quelques lignes à la suite de la création de commentaire dans le programme principale.
If (post.PostAttachment == 1)
{
    List<PhPbB3Attachments> attach = (from atta in entity.PhPbB3Attachments
                 where atta.PostMsgID == post.PostID
                 select atta).ToList();

    foreach (PhPbB3Attachments attachment in attach)
    {
          try
          {
                FileStream fStr = new FileStream(SiteHelper.urlPiecesJointes + attachment.RealFilename, FileMode.Open);
                                     SharepointHelper.AttachToBillet(blog,lookUpBillet,attachment.RealFilename, fStr, attachment.MimeType,lookUpCommentaire);
           }
           catch (Exception ex)
           {
               Console.WriteLine("!!!!! Le fichier "+attachment.RealFilename + " n'existe pas !!!!!");
           }
      }
}

Et enfin la fonction servant à construire le corps du message avec un lien vers la pièce jointe.
public static SPFieldLookupValue AttachToBillet(SPWeb blog,
            SPFieldLookupValue billet, string fileName, Stream str,string mimeType,SPFieldLookupValue commentaire)
{
    SPList liste_concerne;
    SPListItem item_concerne;
    try
    {
        if (commentaire == null) // cas où la pièce jointe appartient au billet.
        {
            liste_concerne = blog.Lists["Billets"];
            item_concerne = liste_concerne.Items.GetItemById(billet.LookupId);
        }
        else
        { //cas où la pièce jointe appartient au commentaire.
            liste_concerne = blog.Lists["Commentaires"];
            item_concerne = liste_concerne.Items.GetItemById(commentaire.LookupId);
        }
        SPList piecesJointes = blog.Lists["Photos"];
        SPFolder folder = piecesJointes.RootFolder;
        //ajoute du fichier dans le dossier de document.
        SPFile newFile = folder.Files.Add(fileName,str, true);
        string item_body = (string)item_concerne["Body"];
        //retrouve l'emplacement de la pièce jointe dans le texte.
        int indexFileName = item_body.IndexOf(fileName);
        string partie_avant = "";
        string partie_apres = "";
        if (indexFileName != -1)
        {
            partie_avant = item_body.Substring(0, indexFileName);
            partie_apres = item_body.Substring(indexFileName);
            partie_avant = partie_avant.Substring(0, partie_avant.LastIndexOf('['));
            partie_apres = partie_apres.Substring(partie_apres.IndexOf(']') + 1);
        }
        string lien_vers_piece_jointe = "";
        //chemin relatif du fichier.
        string fullUrl = SiteHelper.url + newFile.ServerRelativeUrl;
        if (mimeType.Contains("jpeg"))
        {   //si la pièce jointe est une image il faut l'afficher grâce à la balise html <img>
             if (commentaire == null)
                lien_vers_piece_jointe = "<img src='" + fullUrl+ "' alt='" + newFile.Name + "'>";
             else
                 lien_vers_piece_jointe = fullUrl;
        }
        else
        {
            if (mimeType.Contains("application"))
            { //si la pièce jointe est une archive il faut faire un lien faire ce fichier grâce à la balise <html>
                if(commentaire ==null)
                   lien_vers_piece_jointe = "<a href='" + fullUrl + "' title='" + newFile.Name + "'>" + newFile.Name + "</a>";
                else
                   lien_vers_piece_jointe = fullUrl;
            }
        }
        //reconstruit le message autour de la balise de la pièce jointe.
        if(indexFileName == -1)
          item_concerne["Body"] = item_body+ lien_vers_piece_jointe;
        else
         item_concerne["Body"] = partie_avant + lien_vers_piece_jointe + partie_apres;
        item_concerne.Update();
        newFile.Update();
        folder.Update();
   }
   catch(Exception ex)
   {
     Console.WriteLine(ex.ToString());
   }
   return null;
}

Figure 6. Intégration des pièces jointes présentes dans PhpBB

Ajouter les smileys PhpBB

Les smileys présent dans les messages générés par PhpBB se trouvent dans le répertoire « phpBB3\images\smilies »  de votre dossier wamp. Copiez ce répertoire de façon temporaire sur votre serveur SharePoint et préciser le chemin vers ce répertoire dans la classe SiteHelper.

public static string urlSmiley = "C:/Users/spadm/Desktop/importPhpbb/smilies/";

Et ajouter cette fonction dans la classe SharepointHelper:
public static string UploadSmiley(string body,SPWeb blog)
{
   int ind = -1;
            SPList listImage = blog.Lists["Photos"];
            SPFolder folder = listImage.RootFolder;
            while ((ind = body.IndexOf("{SMILIES_PATH}")) >= 0)
            {
                string url = body.Substring(ind-10);
                url = url.Substring(0,url.IndexOf("/>")+2);
                string filName = url.Substring(0,url.IndexOf(".gif") +4);
                filName = filName.Substring(filName.LastIndexOf('/')+1);
        
                SPFile smileyFile = null;
                foreach (SPFile file in folder.Files)
                {
                    if (file.Name == filName)
                        smileyFile = file;
                }
                if (smileyFile == null)
                {
                    FileStream fStr = new FileStream(SiteHelper.urlSmiley+filName, FileMode.Open);
                    smileyFile = folder.Files.Add(filName, fStr, true);
                }
                body = body.Replace(url,
                    "<img alt='"+filName+"' src='"+SiteHelper.url+smileyFile.ServerRelativeUrl+"' style=\"margin:5px\" />");
            }
            return body;
        }

Figure 7. Intégration des smileys PhpBB

Conclusion et petites finitions

Vous aurez remarqué qu’à ce stade quelques balises générées par PhpBB subsistent, celles-ci ne sont pas reconnues par SharePoint lors de la génération des pages Html. Vous pouvez remplacer ces balises par celles correspondantes à la norme Html.
Vous pouvez aussi dans les sources du projet récupérer la fonction Utf8_decode de la classe SiteHelper qui se charge de les remplacer.
Grâce aux fonctionnalités de connections de C# avec toutes sortes de base de données ou fichier de données (Xml, Excel par exemple), vous pouvez importer des données venant de sources diverses.
Ces fonctionnalités permettent à SharePoint d’être encore mieux accepté au niveau d’une entreprise du fait que les sites pourront s’appuyer sur des données préexistante à l’installation de cette nouvelle plateforme.



1 commentaire :