Étape 1 : Les paramètres spécifiques à free.fr

Tout d'abord, je décide de mettre le projet en ligne puis de consulter le dossier web de mon site : Erreur 500. Après vérification, il se trouve que free.fr est en PHP 4 par défaut mais il est possible d'activer le PHP 5 en ajoutant la ligne php 1 dans le fichier web/.htaccess.

Après cette modification faite : Erreur 500. Le problème vient du module Apache mod_rewrite. La suppression des lignes <IfModule mod_rewrite.c> à </IfModule> du fichier web/.htaccess règle le problème. Il ne faut pas oublier de mettre les options no_script_name à off dans le fichier apps/[...]/config/settings.yml car nous n'utilisons désormais plus le module mod_rewrite. Nous voyons maintenant s'afficher les premières lignes : Fatal error: Class 'sfContext' not found in [...]/lib/symfony/util/sfCore.class.php on line 171.

Étape 2 : La fonction glob()

Le wiki de symfony est la première source que j'ai consultée. On nous y explique comment déployer un projet sur un hébergement mutualisé (la version française du wiki ne donnant à ce jour que des informations spécifiques à l'hébergeur OVH). Je n'y ai pas trouvé d'informations très utiles (pour ce cas précis). Je décide alors de faire une recherche et tombe sur un sujet intéressant évoquant le problème qui me concernait. Tout viendrait de la fonction glob() de PHP qui peut être désactivée pour des raisons de sécurité. Je décide donc de tester la fonction de sgue35 (récupérée depuis le site de PHP) en remplaçant l'appel à la fonction glob() par un appel à une fonction locale safe_glob() dans le fichier lib/symfony/config/sfAutoloadConfigHandler.class.php. Pas de chance, ça ne fonctionne toujours pas. Après d'autres recherches, je trouve un sujet sur le forum de symfony. Effaçons les fichiers du dossier cache et essayons cette fonction. Superbe, ça marche.

Cependant, en mode développement, vous pourrez voir une erreur concernant les sessions : Warning: session_start() [<a href='function.session-start'>function.session-start</a>]: open([...]/sessions/sess_2058d289aef1edcc627cf33d5979cd4c, O_RDWR) failed: No such file or directory (2) in [...]/lib/symfony/storage/sfSessionStorage.class.php on line 77. Comme expliqué dans la FAQ de Free, il suffit de créer le dossier sessions à la racine de votre site.

Étape 3 : Activation du plugin sfGuard

Nous avons vu qu'une simple modification de fonction dans un fichier nous a permis d'afficher la page de bienvenue de symfony. Mais une application ne se résume pas à une telle page. Pour mon projet, j'ai décidé d'utiliser le plugin sfGuard. À partir de l'activation de ce module, les choses se corsent mais me permettent d'en apprendre un peu plus sur le fonctionnement de l'initialisation de symfony.

La première erreur à laquelle nous sommes confrontée est Fatal error: sfPropelDatabase::require_once() [function.require]: Failed opening required 'creole/Creole.php' (include_path='[...]/include:.:/usr/php5/lib/php') in [...]/lib/symfony/addon/propel/database/sfPropelDatabase.class.php on line 68. Après analyse du code de symfony, j'ai remarqué que tous les fichiers propres au framework sont chargés en utilisant une URL absolue en se basant sur les paramètres de configuration (sf_symfony_lib_dir etc). Par contre, les fichiers du dossier lib/symfony/vendor (les bibliothèques externes au framework, de ce que j'ai pu voir) partent du principe que les fonctions sont accessibles depuis l'include_path. Hors, comme nous le confirme la FAQ de Free, les fonctions set_include_path et ini_set, utilisées par symfony pour déclarer les chemins des bibliothèques utilisées, sont désactivées. Malgré tout, free.fr définit l'include_path à [...]include:.:/usr/php5/lib/php. Il suffit donc de mettre le contenu du dossier lib/symfony/vendor dans le dossier include de votre projet. Bien, l'étape suivante est le chargement du plugin.

Nous avons désormais l'erreur : Fatal error: Class 'sfGuardSecurityUser' not found in [...]/apps/frontend/lib/myUser.class.php on line 3. Le problème vient du fait que les plugins sont chargés depuis le fichier lib/symfony/config/sfLoader.class.php qui lui aussi utilise la fonction glob(). Ce n'est pas grave, reprenons la méthode que nous avons utilisée pour le fichier lib/symfony/config/sfAutoloadConfigHandler.class.php. Pas de chance, ça ne marche pas.

Après avoir vérifié la fonction safe_glob() trouvée précédemment, j'ai remarqué un problème. En effet, cette fonction suppose qu'on lui donne en argument une chaîne du type /dossier/*, avec le masque à la fin. Hors, symfony appelle la fonction glob() avec des paramètres comme sfConfig::get('sf_plugins_dir').'/*/'.$globalConfigPath. Il a donc fallu refaire cette fonction. Comme elle est utilisée dans plusieurs fichiers, j'ai décidé de la mettre dans lib/symfony/util/sfFinder.class.php.

private static function find_dirs($path, $pattern, &$ret)
{
  if (is_dir($path))
  {
    $dir = opendir($path);
    while (($file = readdir($dir)) !== false)
    {
      if($file != '.' && $file != '..')
      {
        if (is_dir($path.DIRECTORY_SEPARATOR.$file))
        {
          self::find_dirs($path.DIRECTORY_SEPARATOR.$file, $pattern, $ret);
        }
        if (preg_match("#^".strtr(preg_quote($pattern, '#'), array('\*' => '[^/]*', '\?' => '[^/]?', '\[' => '[', '\]' => ']'))."$#i", $path.DIRECTORY_SEPARATOR.$file))
        {
          $ret[] = $path.DIRECTORY_SEPARATOR.$file;
        }
      }
    }
  }
}
public static function glob($pattern)
{
  if (is_dir($pattern))
  {
    return array($pattern);
  }
  $ret = array();
  $pos = strpos($pattern, '*');
  if ($pos > 1)
  {
    $path = substr($pattern, 0, $pos - 1);
    self::find_dirs($path, $pattern, $ret);
  }
  return $ret;
}

La fonction est simple dans la mesure où le but n'était pas de refaire une fonction glob(). Je me suis basé sur une implémentation de fnmatch() en PHP en modifiant .* par [^/]* afin de ne pas prendre en compte les dossiers intermédiaires (plugins/*/lib renvoyait plugins/sfGuardPlugin/lib mais aussi plugins/sfGuardPlugin/modules/sfGuardUser/lib). Une piste à explorer est la classe sfFinder qui fait appel à une classe sfGlobToRegex (contenue dans le même fichier) ressemblant fortement à l'expression régulière ci-dessus.

Il faut maintenant remplacer tous les appels à glob() par sfFinder::glob() dans le fichier lib/symfony/config/sfLoader.class.php. Nous avons désormais accès à la page d'identification.

Malheureusement, nous avons encore une erreur lorsque nous tentons de nous identifier : Fatal error: Class 'BasePeer' not found in [...]/plugins/sfGuardPlugin/lib/model/om/BasesfGuardUserPeer.php on line 73. Cela provient de la fonctionnalité d'auto chargement des classes de symfony. Nous pourrons remarquer que les chemins des bibliothèques sont définis dans le fichier data/symfony/config/autoload.yml. Il faut donc remplacer les lignes path: %SF_SYMFONY_LIB_DIR%/vendor/propel par path: %SF_ROOT_DIR%/include/propel et path: %SF_SYMFONY_LIB_DIR%/vendor/creole par path: %SF_ROOT_DIR%/include/creole afin que symfony recherche dans les bons dossiers.

Nous y sommes presque, il reste une erreur : Warning: BasesfGuardUserPeer::include_once(plugins/sfGuardPlugin/lib/model/map/sfGuardUserMapBuilder.php) [function.BasesfGuardUserPeer-include-once]: failed to open stream: No such file or directory in [...]/plugins/sfGuardPlugin/lib/model/om/BasesfGuardUserPeer.php on line 72. Ce dernier problème est le plus compliqué à résoudre. Ces fichiers sont générés automatiquement par propel et, comme toujours, supposent que le dossier plugins est dans l'include_path. Le fautif se trouve dans le fichier lib/symfony/vendor/propel/Propel.php. Il faut remplacer la ligne $ret = include_once($path); par $ret = include_once(sfConfig::get('sf_root_dir').'/'.$path);.

Dommage, il reste une erreur : Warning: BasesfGuardUserPeer::include_once(plugins/sfGuardPlugin/lib/model/map/sfGuardUserMapBuilder.php) [function.BasesfGuardUserPeer-include-once]: failed to open stream: No such file or directory in [...]/plugins/sfGuardPlugin/lib/model/om/BasesfGuardUserPeer.php on line 72. La seule solution que j'ai trouvée pour l'instant est de remplacer les occurrences de include_once ' par include_once sfConfig::get('sf_root_dir').' et require_once ' par require_once sfConfig::get('sf_root_dir').'. Une commande bash fera le travail pour nous :

for dir in `find -type d -name 'om' | grep -v 'symfony'`; do for i in ${dir}/*; do cat $i | sed "s/include_once '/include_once sfConfig::get('sf_root_dir').'\//g" > $i.tmp; rm $i; mv $i.tmp $i; done; done
for dir in `find -type d -name 'om' | grep -v 'symfony'`; do for i in ${dir}/*; do cat $i | sed "s/require_once '/require_once sfConfig::get('sf_root_dir').'\//g" > $i.tmp; rm $i; mv $i.tmp $i; done; done

Étape 4 : Sécurisation

Un problème majeur de cette installation est la mise à disposition de fichiers configuration contenant des mots de passe (comme le fichier databases.yml). Pour y remédier, vous devez créer un fichier .htaccess dans le dossier config contenant :

Order deny,allow
deny from all

De cette façon, les utilisateurs n'auront pas accès au dossier. Il pourrait y avoir d'autres fichiers contenant des données sensibles (ce n'est pas pour rien que tout ce qui est publique est dans le dossier web). Il serait cependant fastidieux (et source d'oubli) de mettre un fichier .htaccess dans chaque dossier ne devant pas être accessible. Il existe heureusement une solution : tout mettre dans un dossier lib.

Je vais donc faire un résumé des dossiers à placer sur votre site.

/         -> le contenu du dossier web
/lib      -> le reste des dossiers SAUF le dossier lib/symfony/vendor
/include  -> le contenu du dossier lib/symfony/vendor
/sessions -> dossier à créer pour l'utilisation des sessions

Dans cette configuration, vous avez juste à placer le fichier .htaccess dont nous avons parlé dans les dossiers lib et include. Afin d'utiliser cette organisation, nous devrons tout de même modifier quelques fichiers.

Tout d'abord, le fichier data/symfony/config/autoload.yml : remplacer les lignes path: %SF_ROOT_DIR%/include/propel par path: %SF_ROOT_DIR%/../include/propel et path: %SF_ROOT_DIR%/include/creole par path: %SF_ROOT_DIR%/../include/creole. Ensuite, les fichiers .php à la racine (index.php, frontend_dev.php si votre application s'appelle frontend etc) : remplacer la ligne define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..')); par define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/lib'));.

Et voilà, vous avez une application sécurisée et à la racine de votre site qui plus est.

Pour faire court

J'ai préparé 2 patches permettant d'appliquer les modifications que j'ai évoquées. Le premier corrige la restriction de la fonction glob(). Le deuxième corrige la restriction des fonctions ini_set et set_include_path. Le reste étant spécifique à chaque installation, je vous laisse travailler...