array()); // forcer des répertoires en dehors des options de ligne de commande private $commands = array(); // forcer des commandes spécifiques à phpdocumentor // les infos de plugins sont enregistrées dedans pour l'index.html de la commande generateFromFile() private $infos_plugins = array(); /** * Description de la documentation à générer * @var string $description */ private $description = ""; /** * Présentation de la documentation à générer * @var string $presentation */ private $presentation = ""; /** * Constructeur * * @param InputInterface $input * @param OutputInterface $output **/ public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->output = $output; $this->setApplicationDirectories(); } /** * Générer la documentation à partir du SVN de SPIP * * Retrouve le nom de la version utilisée et s'en sert de titre. * * @param string $chemin Chemin dans le svn **/ public function generateFromSpip($chemin) { $core = "svn://trac.rezo.net/spip/$chemin"; $prefixe = $this->input->getOption('prefixe'); $ok = $this->createDirectories($prefixe) && $this->getSvnSource($core); if (!$ok) return false; $titre = ""; if ($this->input->hasOption('titre')) { $titre = $this->input->getOption('titre'); } // retrouver la version de SPIP if (!$titre and file_exists($inc_version = $this->dirs['input'] . '/ecrire/inc_version.php')) { $inc_version = file_get_contents($inc_version); if (preg_match('/spip_version_branche = "([^"]+)";/', $inc_version, $res)) { $titre = "SPIP " . $res[1]; } } if ($titre) { $this->setCommand('title', $titre); $this->setOption('titre', $titre); $this->setOption('description', "Documentation du code PHP de " . $titre); } return $this->generateFromDirectory($this->dirs['input'], true); } /** * Générer la documentation à partir du SVN de la Zone de SPIP * * @param string $chemin Chemin dans le svn **/ public function generateFromZone($chemin) { $zone = "svn://zone.spip.org/spip-zone/$chemin"; return $this->generateFromSvn($zone); } /** * Générer la documentation à partir d'une url SVN * * @param string $source URL SVN **/ public function generateFromSvn($source) { $prefixe = $this->input->getOption('prefixe'); $ok = $this->createDirectories($prefixe) && $this->getSvnSource($source) && $this->generateFromDirectory($this->dirs['input'], true); return $ok; } /** * Générer la documentation à partir d'un répertoire indiqué * * @param string $dir Chemin du répertoire source * @param bool $is_ready * Indique si les préparatifs (création des répertoires) sont déjà faits **/ public function generateFromDirectory($dir, $is_ready = false) { $prefixe = $this->input->getOption('prefixe'); $ok = true; if (!$is_ready) { # forcer le chemin de la source spécifique $this->options['dirs']['input'] = $dir; $ok = $this->createDirectories($prefixe); } $ok = $ok && $this->retrouverInfoPaquetXml() && $this->prepareConfigXml() && $this->clearLogs() ; if (!$ok) { return false; } $this->execute(); return true; } /** * Générer la documentation à partir d'un fichier listant les documentations à exécuter * * On duplique le fichier chez soi et on gère une rotation de ce fichier à chaque lancement * afin de supprimer les documentations devenues inutiles (disparues du nouveau fichier). * * @param string $file Chemin du fichier ou URL SVN **/ public function generateFromFile($file) { // définir les chemins et faire tourner le backup précédent $this->files['autodoc.txt'] = $this->dirs['work'] . '/autodoc.txt'; $this->files['autodoc.txt.bak'] = $this->files['autodoc.txt'] . '.bak'; if (file_exists( $this->files['autodoc.txt.bak'] )) { unlink($this->files['autodoc.txt.bak']); } if (file_exists( $this->files['autodoc.txt'] )) { copy($this->files['autodoc.txt'], $this->files['autodoc.txt.bak']); unlink($this->files['autodoc.txt']); } // copier le fichier de liste chez soi if (0 === strpos($file, 'svn://')) { $this->output->write("* Obtenir $file "); // si c'est en svn, on l'exporte simplement. if ($res = $this->makeSvnCommand("export $file autodoc.txt", true, $this->dirs['work'])) { $this->output->writeln("[OK]"); } else { $this->output->writeln("[Error]"); throw new \Exception("Impossible de récupérer le fichier SVN : $file"); } } else { if (file_exists($file)) { copy($file, $this->files['autodoc.txt']); } else { throw new \Exception("Impossible de trouver le fichier : $file"); } } $this->output->writeln("\n* Lecture des informations du fichier"); $anciens = $this->parseFile($this->files['autodoc.txt.bak'], false); $presents = $this->parseFile($this->files['autodoc.txt']); $output_base = $this->input->getOption('sorties'); if (!$output_base) $output_base = $this->dirs['work'] . '/output'; if ($anciens) { $absents = array_diff_key($anciens, $presents); if ($absents) { $this->output->writeln(" Certaines documentations ne sont plus à générer."); foreach ($absents as $prefixe=>$absent) { $this->output->writeln(" - Effacement de $prefixe."); $this->deleteDirectoryContent($output_base . '/$prefixe', true); $this->deleteDirectoryContent($this->dirs['work'] . 'log/$prefixe', true); $this->deleteDirectoryContent($this->dirs['work'] . 'input/$prefixe', true); $this->deleteDirectoryContent($this->dirs['work'] . 'cache/$prefixe', true); } } } $nb_erreur = 0; foreach ($presents as $prefixe => $present) { $this->output->writeln("\n\n"); $this->output->writeln(" Générer la documentation de $prefixe"); $this->output->writeln(" ------------------------------------------------"); $this->output->writeln("\n"); $ok = $this->createDirectories($prefixe) && $this->getSvnSource($present['source']) && $this->retrouverInfoPaquetXml() && $this->prepareConfigXml() && $this->clearLogs() ; if (!$ok) { $nb_erreur++; $this->output->writeln(" * La documentation de $prefixe est ignorée à cause d'une erreur."); continue; } $this->execute(); } $this->output->writeln("\n\n\n"); if ($nb_erreur) { $this->output->writeln("$nb_erreur documentation(s) non générée(s) sur " . count($presents) . "."); } else { $this->output->writeln("Toutes les documentations (" . count($presents) . ") ont été générées.\n"); } // générer un sommaire de toutes ces documentations $this->output->writeln("Création du sommaire des documentations"); // ajouter les dépendances du fichier html if (!is_dir($data = $output_base . '/__data')) { mkdir($data); $template = $this->dirs['template']; foreach (array('bootstrap', 'images', 'css', 'js') as $dir) { exec("cp -r '$template/$dir' '$data/$dir'"); } copy("$template/favicon.png", "$output_base/favicon.png"); } if (file_exists($index = "$output_base/index.html")) { unlink($index); } // obtenir les description des plugins $plugins = array(); foreach ($presents as $prefixe => $present) { if (isset($this->infos_plugins[$prefixe])) { $plugins[$prefixe] = $this->infos_plugins[$prefixe]; } else { // ce n'était pas un plugin ou erreur $plugins[$prefixe] = array( 'prefixe' => $prefixe, 'nom' => $prefixe, 'slogan' => '', 'description' => '', 'annuaire' => '', 'documentation' => '', 'developpement' => '', ); } } usort($plugins, function($a, $b) { return ($a['nom'] < $b['nom']) ? -1 : 1; }); // charger Twig, générer la page et l'enregitrer $loader = new \Twig_Loader_Filesystem( $this->dirs['helper'] . '/Template' ); $twig = new \Twig_Environment($loader); $content = $twig->render('index.html', array( 'titre' => 'Documentation automatique des plugins SPIP', 'plugins' => $plugins, )); file_put_contents($index, $content); $this->output->writeln("Fin"); } /** * Analyse un fichier listant les documentations à générer. * * @param string $file Chemin du fichier * @param bool $write_errors Ecrire les erreurs dans la console ? * @return array|bool * - false si echec, * - couples (prefixe => array) sinon. Le tableau de description a les clés suivantes : * - ligne : numéro de ligne * - type : 'svn' * - source : url du svn **/ private function parseFile($file, $write_errors = true) { if (!file_exists($file)) return false; if (!$lines = file($file)) return false; $liste = array(); foreach ($lines as $lineno => $line) { if (!$line) continue; $line = trim($line); if (!$line OR $line[0] == '#') continue; $couples = explode(';', $line); $lineno++; if (count($couples) != 2) { if (count($couples) == 1) { $this->output->writeln("Ligne $lineno omise. Le préfixe ne semble pas défini. Contenu : $line"); } else { $this->output->writeln("Ligne $lineno omise. Trop de paramètres indiqués. Contenu : $line"); } continue; } list ($url, $prefixe) = $couples; if (!$url) { $this->output->writeln("Ligne $lineno omise. L'URL ne semble pas définie. Contenu : $line"); continue; } if (!$prefixe) { $this->output->writeln("Ligne $lineno omise. Le préfixe ne semble pas défini. Contenu : $line"); continue; } if (isset($liste[$prefixe])) { $this->output->writeln("Ligne $lineno omise. Le prefixe $prefixe est déjà déclaré ligne : " . $liste[$prefixe]['ligne'] . ""); continue; } // pas d'erreur ! $liste[$prefixe] = array( 'type' => 'svn', 'ligne' => $lineno, 'source' => $url ); } return $liste; } /** * Effacer le contenu du répertoire log. * * On ne gardera du coup que les logs de la prochaine exécution. * * @return bool true si réussi **/ private function clearLogs() { return $this->deleteDirectoryContent($this->dirs['log']); } /** * Définit les répertoires utiles à l'application **/ private function setApplicationDirectories() { # ce répertoire $this->dirs['helper'] = realpath(__DIR__); # executable php de l'application autodoc (extension de l'application phpdocumentor) $this->dirs['bin'] = realpath( $this->dirs['helper'] . '/../../../bin'); # répertoire racine (celui depuis lequel on execute ce script). exec('pwd', $output); $this->dirs['root'] = $output[0]; # répertoire de travail (celui où on écrira tout). $this->dirs['work'] = $this->dirs['root'] . '/work'; # répertoire du template zora $this->dirs['template'] = realpath( $this->dirs['helper'] . '/../../../templates/zora'); # fichier de config xml pour phpdocumentor $this->files['phpdoc.xml'] = $this->dirs['work'] . '/phpdoc.xml'; } /** * Définit et crée les répertoires nécessaires au fonctionnement de ce programme * * @param string $prefixe Préfixe utilisé pour cette génération * @return bool true si réussi **/ private function createDirectories($prefixe) { $_work = $this->dirs['work']; $this->output->writeln("* Vérifier/créer les répertoires de travail dans $_work"); # si un répertoire pour toutes les sorties est indiqué, forcer son utilisation. if ($this->input->hasOption('sorties') and $dir_output = $this->input->getOption('sorties')) { $this->setOption('dirs/output', $dir_output . "/$prefixe"); } # si un répertoire de sortie est indiqué, forcer son utilisation. if ($this->input->hasOption('sortie') and $dir_output = $this->input->getOption('sortie')) { $this->setOption('dirs/output', $dir_output); } foreach (array('output', 'input', 'log', 'cache') as $dir) { // valeur par défaut, en fonction du préfixe $this->dirs[$dir] = "$_work/$dir/$prefixe"; // valeur forcée dans certains cas if ($path = $this->getOption("dirs/$dir")) { $this->dirs[$dir] = $path; } } foreach (array('output', 'input', 'log', 'cache') as $dir) { if (!$this->createDirectory( $this->dirs[$dir] )) { return false; } } return true; } /** * Créer un des répertoires d'utilisation * * Vérifie également que le répertoire est utilisable en écriture. * * @param string $chemin Répertoire à créer * @return bool true si réussi **/ private function createDirectory($dir) { if (!is_dir($dir)) { $this->output->writeln(" - Création du répertoire $dir"); if (!@mkdir($dir, 0755, true)) { $this->output->writeln("Impossible de créer le répertoire : $dir"); $error = error_get_last(); $this->output->writeln($error['message']); return false; } } if (!is_writable($dir)) { $this->output->writeln("Le répertoire $dir n'est pas accessible en écriture"); return false; } return true; } /** * Télécharge ou met à jour la source SVN indiquée * * @param string $source URL de la source SVN * @return bool true si réussi **/ private function getSvnSource($source) { $this->output->writeln("* Obtenir $source"); // si c'est en svn et le bon, on fait svn up simplement. if ($res = $this->makeSvnCommand('info --xml')) { // nous avons un svn… verifions que c'est le bon ! $xml = simplexml_load_string(implode("", $res)); $source_actuelle = (string)$xml->entry->url; if ($source_actuelle == $source) { $this->output->write(" - Update "); if (!$res = $this->makeSvnCommand("update")) { $this->output->writeln("[Echec]"); return false; } $last = array_pop($res); $this->output->writeln("[OK] ($last)"); return true; } } // erreur, donc pas un dossier svn ou pas le bon // on nettoie le contenu pour svn co $this->deleteDirectoryContent($this->dirs['input']); $this->output->write(" - Checkout "); if (!$res = $this->makeSvnCommand("checkout $source .")) { $this->output->writeln("[Echec]"); return false; } $last = array_pop($res); if ($last) $last = "($last)"; $this->output->writeln("[OK] $last"); return true; } /** * Exécuter la commande svn indiquée * * @param string $cmd Commande SVN * @param bool $no_error true : Envoie les erreurs dans /dev/null * @return mixed|bool false en cas d'erreur **/ private function makeSvnCommand($cmd, $no_error = true, $dir = null) { if (is_null($dir)) { $dir = $this->dirs['input']; } $svn = '/usr/bin/svn'; $no_error = $no_error ? '2> /dev/null' : ''; exec("cd $dir && $svn $cmd $no_error", $res, $error); if ($error) return false; return $res; } /** * Suppression du contenu d'un repertoire. * * @link http://www.php.net/manual/en/function.rmdir.php#92050 * * @param string $dir Chemin du repertoire * @param string $delete_me Supprimer aussi le répertoire ? * @return bool Suppression reussie. */ function deleteDirectoryContent($dir, $delete_me = false) { if (!file_exists($dir)) return true; if (!is_dir($dir) || is_link($dir)) return @unlink($dir); foreach (scandir($dir) as $item) { if ($item == '.' || $item == '..') continue; if (!$this->deleteDirectoryContent($dir . "/" . $item, true)) { @chmod($dir . "/" . $item, 0777); if (!$this->deleteDirectoryContent($dir . "/" . $item, true)) return false; }; } if ($delete_me) { return @rmdir($dir); } return true; } /** * Obtient une option, soit de la ligne de commande, soit forcée par le générateur * * @param string $name Nom de l'option * @return mixed **/ private function getOption($name, $default = null) { if (strpos($name, '/') === false) { if ($this->input->hasOption($name) and $valeur = $this->input->getOption($name)) { return $valeur; } if (isset($this->options[$name]) and $this->options[$name]) { return $this->options[$name]; } return $default; } $name = explode('/', $name); $pointeur = &$this->options; while (true) { $n = array_shift($name); if (!isset($pointeur[$n])) { return $default; } if (!count($name)) { return $pointeur[$n]; } if (!is_array($pointeur[$n])) { return $default; } $pointeur = &$pointeur[$n]; } } /** * Enregistre une option (forcée par le générateur) * * Permet d'enregistrer avec des chemins par / * * @example * $this->setOption('dirs/output', $dir) * * @param string $name Nom de l'option * @param string $value Valeur de l'option **/ private function setOption($name, $value) { $name = explode('/', $name); $n = array_shift($name); $pointeur = &$this->options; while ($name) { if (!is_array($pointeur[$n])) { $pointeur[$n] = array(); } $pointeur = &$pointeur[$n]; $n = array_shift($name); } $pointeur[$n] = $value; } /** * Récupère une option de commande pour phpDocumentor * * @param string $command Nom de la commande * @return mixed **/ private function getCommand($command) { if (isset($this->commands[$command])) { $val = $this->commands[$command]; return " --$command=\"$val\""; } return null; } /** * Enregistre une option de commande pour phpDocumentor * * @param string $command Nom de la commande * @param string $value Valeur de la commande **/ private function setCommand($command, $value) { $this->commands[$command] = $value; } /** * Retrouver des infos du paquet.xml si on le trouve * * Définir avec le titre, et une présentation (si ce n'est déjà fait). * * @return bool true si on a trouvé un paquet.xml **/ private function retrouverInfoPaquetXml() { $source = $this->dirs['input']; $is_spip = false; if (!file_exists($paquet = $source . '/paquet.xml')) { $is_spip = true; if (!file_exists($paquet = $source . '/ecrire/paquet.xml')) { return false; } } $paquet = simplexml_load_file($paquet); if (!$paquet) return false; $this->output->writeln(" > Paquet.xml trouvé."); $nom = (string)$paquet->nom; $version = (string)$paquet['version']; $prefixe = (string)$paquet['prefix']; $this->infos_plugins[$prefixe] = array( 'nom' => $nom, 'prefixe' => $prefixe, 'slogan' => '', 'description' => '', 'annuaire' => '', 'documentation' => '', 'developpement' => '', ); $documentation = $developpement = $lien_plugins_spip = ''; if (isset($paquet['documentation'])) { $documentation = $paquet['documentation']; $this->infos_plugins[$prefixe]['documentation'] = $documentation; } if (isset($paquet['developpement'])) { $developpement = $paquet['developpement']; $this->infos_plugins[$prefixe]['developpement'] = $developpement; } if (!$is_spip) { $lien_plugins_spip = "http://plugins.spip.net/$prefixe.html"; $this->infos_plugins[$prefixe]['annuaire'] = $lien_plugins_spip; // récupérer les infos dans les chaînes de langue. $langue = $source . "/lang/paquet-$prefixe"."_fr.php"; if (file_exists($langue)) { $GLOBALS['idx_lang'] = 'pour_autodoc'; defined('_ECRIRE_INC_VERSION') || define('_ECRIRE_INC_VERSION', 1); @include $langue; $infos = $GLOBALS['pour_autodoc']; if (is_array($infos)) { if (!$nom and isset($infos["$prefixe" . "_nom"])) { $nom = $infos["$prefixe" . "_nom"]; $this->infos_plugins[$prefixe]['nom'] = $nom; } if (isset($infos["$prefixe" . "_slogan"])) { $this->infos_plugins[$prefixe]['slogan'] = $infos["$prefixe" . "_slogan"]; } if (isset($infos["$prefixe" . "_descriptif"])) { $this->infos_plugins[$prefixe]['descriptif'] = $infos["$prefixe" . "_descriptif"]; } } } } if (!$this->getOption('titre') and $nom) { $this->setOption('titre', $nom); } if (!$this->getOption('presentation')) { if ($is_spip) { $presentation = "Cette documentation est issue du code source PHP de SPIP $version. "; } else { $presentation = "Cette documentation est issue du code source PHP du plugin « ".$nom." », version $version. "; } if (!$is_spip OR $documentation OR $developpement) { if (!$is_spip) { $presentation .= "- [Description dans l'annuaire des plugins]($lien_plugins_spip) "; } if ($documentation) { $presentation .= "- [Documentation]($documentation) "; } if ($developpement) { $presentation .= "- [Outil de développement]($developpement) "; } } $this->setOption('presentation', $presentation); } return true; } /** * Utilise le template de configuration phpdoc.xml en modifiant ses variables * et l'enregistre pour qu'il soit utilisé par l'application * * @return bool true si réussi. **/ private function prepareConfigXml() { $this->output->write("* Préparer le fichier phpdoc.xml "); $template = $this->dirs['helper'] . '/phpdoc_helper.xml'; $template = file_get_contents($template); $substitutions = array( '@DIR_CACHE@' => $this->dirs['cache'], '@DIR_OUTPUT@' => $this->dirs['output'], '@DIR_INPUT@' => $this->dirs['input'], '@DIR_LOG@' => $this->dirs['log'], '@DIR_ROOT@' => $this->dirs['root'], '@OPT_TITRE@' => $this->getOption('titre', ''), '@OPT_DESCRIPTION@' => $this->getOption('description', ''), '@OPT_PRESENTATION@' => $this->getOption('presentation', ''), ); $template = str_replace(array_keys($substitutions), array_values($substitutions), $template); $destination = $this->dirs['work'] . '/phpdoc.xml'; @unlink($destination); if (!@file_put_contents($this->files['phpdoc.xml'], $template)) { $this->output->writeln("[Echec]"); return false; } $this->output->writeln("[OK]"); return true; } /** * Exécute l'application phpDocumentor avec le phpdoc.xml qui a été créé * * @return bool true si réussi. **/ private function execute() { $conf = $this->files['phpdoc.xml']; $command = "project:run --config=$conf --parseprivate"; if ($title = $this->getCommand('title')) { $command .= $title; } $app = new Application(); $app->run_with_command($command); } }