1007225e5Sgerardnico<?php 2007225e5Sgerardnico/** 3007225e5Sgerardnico * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4007225e5Sgerardnico * 5007225e5Sgerardnico * This source code is licensed under the GPL license found in the 6007225e5Sgerardnico * COPYING file in the root directory of this source tree. 7007225e5Sgerardnico * 8007225e5Sgerardnico * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9007225e5Sgerardnico * @author ComboStrap <support@combostrap.com> 10007225e5Sgerardnico * 11007225e5Sgerardnico */ 12007225e5Sgerardnico 134ebc3257Sgerardnicouse ComboStrap\DatabasePageRow; 14c3437056SNickeauuse ComboStrap\Event; 1504fd306cSNickeauuse ComboStrap\ExceptionBadSyntax; 1604fd306cSNickeauuse ComboStrap\ExceptionCompile; 17031d4b49Sgerardnicouse ComboStrap\ExceptionNotExists; 1804fd306cSNickeauuse ComboStrap\ExceptionNotFound; 1904fd306cSNickeauuse ComboStrap\ExceptionRuntime; 20ee419e0eSNicolas GERARDuse ComboStrap\ExceptionSqliteNotAvailable; 21031d4b49Sgerardnicouse ComboStrap\ExecutionContext; 2237748cd8SNickeauuse ComboStrap\FsWikiUtility; 23c3437056SNickeauuse ComboStrap\LogUtility; 2404fd306cSNickeauuse ComboStrap\MarkupPath; 25031d4b49Sgerardnicouse ComboStrap\Meta\Field\BacklinkCount; 2604fd306cSNickeauuse ComboStrap\Meta\Field\PageH1; 27c3437056SNickeauuse ComboStrap\MetadataFrontmatterStore; 2871f916b9Sgerardnicouse ComboStrap\Sqlite; 29007225e5Sgerardnicouse splitbrain\phpcli\Options; 30007225e5Sgerardnico 31e8b2ff59SNickeau/** 3204fd306cSNickeau * All dependency are loaded 33e8b2ff59SNickeau */ 3404fd306cSNickeaurequire_once(__DIR__ . '/vendor/autoload.php'); 35007225e5Sgerardnico 36007225e5Sgerardnico/** 37007225e5Sgerardnico * The memory of the server 128 is not enough 38007225e5Sgerardnico */ 39007225e5Sgerardnicoini_set('memory_limit', '256M'); 40007225e5Sgerardnico 41c3437056SNickeau 42007225e5Sgerardnico/** 43007225e5Sgerardnico * Class cli_plugin_combo 44007225e5Sgerardnico * 45007225e5Sgerardnico * This is a cli: 46007225e5Sgerardnico * https://www.dokuwiki.org/devel:cli_plugins#example 47007225e5Sgerardnico * 48007225e5Sgerardnico * Usage: 49007225e5Sgerardnico * 50007225e5Sgerardnico * ``` 51007225e5Sgerardnico * docker exec -ti $(CONTAINER) /bin/bash 52c3437056SNickeau * ``` 53c3437056SNickeau * ``` 54c3437056SNickeau * set animal=animal-directory-name 55c3437056SNickeau * php ./bin/plugin.php combo --help 56007225e5Sgerardnico * ``` 57007225e5Sgerardnico * or via the IDE 58007225e5Sgerardnico * 59007225e5Sgerardnico * 60007225e5Sgerardnico * Example: 61007225e5Sgerardnico * https://www.dokuwiki.org/tips:grapher 62007225e5Sgerardnico * 63007225e5Sgerardnico */ 64007225e5Sgerardnicoclass cli_plugin_combo extends DokuWiki_CLI_Plugin 65007225e5Sgerardnico{ 66c3437056SNickeau 67c3437056SNickeau const METADATA_TO_DATABASE = "metadata-to-database"; 6871f916b9Sgerardnico const ANALYTICS = "analytics"; 69c3437056SNickeau const METADATA_TO_FRONTMATTER = "metadata-to-frontmatter"; 70ee419e0eSNicolas GERARD const BROKEN_LINKS = "broken-links"; 7171f916b9Sgerardnico const SYNC = "sync"; 72c3437056SNickeau const PLUGINS_TO_UPDATE = "plugins-to-update"; 73c3437056SNickeau const FORCE_OPTION = 'force'; 74c3437056SNickeau const PORT_OPTION = 'port'; 75c3437056SNickeau const HOST_OPTION = 'host'; 76edc35203Sgerardnico const CANONICAL = "combo-cli"; 77c3437056SNickeau 78007225e5Sgerardnico 79007225e5Sgerardnico /** 80007225e5Sgerardnico * register options and arguments 81007225e5Sgerardnico * @param Options $options 8282a60d03SNickeau * 8382a60d03SNickeau * Note the animal is set in {@link DokuWikiFarmCore::detectAnimal()} 8482a60d03SNickeau * via the environment variable `animal` that is passed in the $_SERVER variable 85007225e5Sgerardnico */ 86007225e5Sgerardnico protected function setup(Options $options) 87007225e5Sgerardnico { 88c3437056SNickeau $help = <<<EOF 89c3437056SNickeauComboStrap Administrative Commands 90c3437056SNickeau 91c3437056SNickeau 92c3437056SNickeauExample: 93c3437056SNickeau * Replicate all pages into the database 94c3437056SNickeau```bash 9504fd306cSNickeauphp ./bin/plugin.php combo metadata-to-database --host serverHostName --port 80 : 96c3437056SNickeau# or 9704fd306cSNickeauphp ./bin/plugin.php combo metadata-to-database --host serverHostName --port 80 / 98c3437056SNickeau``` 99c3437056SNickeau * Replicate only the page `:namespace:my-page` 100c3437056SNickeau```bash 10104fd306cSNickeauphp ./bin/plugin.php combo metadata-to-database --host serverHostName --port 80 :namespace:my-page 102c3437056SNickeau# or 10304fd306cSNickeauphp ./bin/plugin.php combo metadata-to-database --host serverHostName --port 80 /namespace/my-page 104c3437056SNickeau``` 105c3437056SNickeau 106c3437056SNickeauAnimal: If you want to use it for an animal farm, you need to set first the animal directory name in a environment variable 107c3437056SNickeau```bash 1084cadd4f8SNickeauanimal=animal-directory-name php ./bin/plugin.php combo 109c3437056SNickeau``` 110c3437056SNickeau 111c3437056SNickeauEOF; 112c3437056SNickeau 113ee419e0eSNicolas GERARD /** 114ee419e0eSNicolas GERARD * Global Options 115ee419e0eSNicolas GERARD */ 116c3437056SNickeau $options->setHelp($help); 117007225e5Sgerardnico $options->registerOption('version', 'print version', 'v'); 118ee419e0eSNicolas GERARD /** @noinspection PhpRedundantOptionalArgumentInspection */ 119ee419e0eSNicolas GERARD $options->registerOption( 120ee419e0eSNicolas GERARD 'dry', 121ee419e0eSNicolas GERARD "Optional, dry-run", 122ee419e0eSNicolas GERARD 'd', false); 123007225e5Sgerardnico $options->registerOption( 124007225e5Sgerardnico 'output', 125007225e5Sgerardnico "Optional, where to store the analytical data as csv eg. a filename.", 126c3437056SNickeau 'o', 127c3437056SNickeau true 128c3437056SNickeau ); 129ee419e0eSNicolas GERARD 130ee419e0eSNicolas GERARD /** 131ee419e0eSNicolas GERARD * Command without options 132ee419e0eSNicolas GERARD */ 133ee419e0eSNicolas GERARD $options->registerCommand(self::ANALYTICS, "Start the analytics and export optionally the data"); 134ee419e0eSNicolas GERARD $options->registerCommand(self::PLUGINS_TO_UPDATE, "List the plugins to update"); 135ee419e0eSNicolas GERARD $options->registerCommand(self::BROKEN_LINKS, "Output Broken Links"); 136ee419e0eSNicolas GERARD 137ee419e0eSNicolas GERARD 138ee419e0eSNicolas GERARD // Metadata to database command 139ee419e0eSNicolas GERARD $options->registerCommand(self::METADATA_TO_DATABASE, "Replicate the file system metadata into the database"); 140007225e5Sgerardnico $options->registerOption( 141c3437056SNickeau self::HOST_OPTION, 142c3437056SNickeau "The http host name of your server. This value is used by dokuwiki in the rendering cache key", 143c3437056SNickeau null, 144c3437056SNickeau true, 145c3437056SNickeau self::METADATA_TO_DATABASE 146c3437056SNickeau ); 147c3437056SNickeau $options->registerOption( 148c3437056SNickeau self::PORT_OPTION, 149c3437056SNickeau "The http host port of your server. This value is used by dokuwiki in the rendering cache key", 150c3437056SNickeau null, 151c3437056SNickeau true, 152c3437056SNickeau self::METADATA_TO_DATABASE 153c3437056SNickeau ); 154c3437056SNickeau $options->registerOption( 155c3437056SNickeau self::FORCE_OPTION, 156c3437056SNickeau "Replicate with force", 157c3437056SNickeau 'f', 158c3437056SNickeau false, 159c3437056SNickeau self::METADATA_TO_DATABASE 160c3437056SNickeau ); 16191f20ee4SNicolas GERARD $startPathArgName = 'startPath'; 16291f20ee4SNicolas GERARD $startPathHelpDescription = "The start path (a page or a directory). For all pages, type the root directory '/' or ':'"; 16391f20ee4SNicolas GERARD $options->registerArgument( 16491f20ee4SNicolas GERARD $startPathArgName, 16591f20ee4SNicolas GERARD $startPathHelpDescription, 16691f20ee4SNicolas GERARD true, 16791f20ee4SNicolas GERARD self::METADATA_TO_DATABASE 16891f20ee4SNicolas GERARD ); 169ee419e0eSNicolas GERARD 170ee419e0eSNicolas GERARD 171ee419e0eSNicolas GERARD // Metadata Command definition 172ee419e0eSNicolas GERARD $options->registerCommand(self::METADATA_TO_FRONTMATTER, "Replicate the file system metadata into the page frontmatter"); 17391f20ee4SNicolas GERARD $options->registerArgument( 17491f20ee4SNicolas GERARD $startPathArgName, 17591f20ee4SNicolas GERARD $startPathHelpDescription, 17691f20ee4SNicolas GERARD true, 17791f20ee4SNicolas GERARD self::METADATA_TO_FRONTMATTER 17891f20ee4SNicolas GERARD ); 179ee419e0eSNicolas GERARD 180ee419e0eSNicolas GERARD // Sync Command Definition 181ee419e0eSNicolas GERARD $options->registerCommand(self::SYNC, "Delete the non-existing pages in the database"); 18291f20ee4SNicolas GERARD $options->registerArgument( 18391f20ee4SNicolas GERARD $startPathArgName, 18491f20ee4SNicolas GERARD $startPathHelpDescription, 18591f20ee4SNicolas GERARD true, 18691f20ee4SNicolas GERARD self::SYNC 18791f20ee4SNicolas GERARD ); 188007225e5Sgerardnico 189007225e5Sgerardnico } 190007225e5Sgerardnico 191007225e5Sgerardnico /** 192007225e5Sgerardnico * The main entry 193007225e5Sgerardnico * @param Options $options 194007225e5Sgerardnico */ 195007225e5Sgerardnico protected function main(Options $options) 196007225e5Sgerardnico { 197007225e5Sgerardnico 198007225e5Sgerardnico 19982a60d03SNickeau if (isset($_REQUEST['animal'])) { 2004cadd4f8SNickeau // on linux 20182a60d03SNickeau echo "Animal detected: " . $_REQUEST['animal'] . "\n"; 20282a60d03SNickeau } else { 2034cadd4f8SNickeau // on windows 20482a60d03SNickeau echo "No Animal detected\n"; 20582a60d03SNickeau echo "Conf: " . DOKU_CONF . "\n"; 20682a60d03SNickeau } 20782a60d03SNickeau 208c3437056SNickeau $args = $options->getArgs(); 209c3437056SNickeau 210c3437056SNickeau 21171f916b9Sgerardnico $depth = $options->getOpt('depth', 0); 21221913ab3SNickeau $cmd = $options->getCmd(); 2134ebc3257Sgerardnico 2144ebc3257Sgerardnico try { 21521913ab3SNickeau switch ($cmd) { 216c3437056SNickeau case self::METADATA_TO_DATABASE: 217c3437056SNickeau $startPath = $this->getStartPath($args); 218c3437056SNickeau $force = $options->getOpt(self::FORCE_OPTION, false); 219c3437056SNickeau $hostOptionValue = $options->getOpt(self::HOST_OPTION, null); 220c3437056SNickeau if ($hostOptionValue === null) { 221c3437056SNickeau fwrite(STDERR, "The host name is mandatory"); 222c3437056SNickeau return; 223c3437056SNickeau } 224c3437056SNickeau $_SERVER['HTTP_HOST'] = $hostOptionValue; 225c3437056SNickeau $portOptionName = $options->getOpt(self::PORT_OPTION, null); 226c3437056SNickeau if ($portOptionName === null) { 227c3437056SNickeau fwrite(STDERR, "The host port is mandatory"); 228c3437056SNickeau return; 229c3437056SNickeau } 230c3437056SNickeau $_SERVER['SERVER_PORT'] = $portOptionName; 231c3437056SNickeau $this->index($startPath, $force, $depth); 232c3437056SNickeau break; 233c3437056SNickeau case self::METADATA_TO_FRONTMATTER: 234c3437056SNickeau $startPath = $this->getStartPath($args); 235c3437056SNickeau $this->frontmatter($startPath, $depth); 236c3437056SNickeau break; 237ee419e0eSNicolas GERARD case self::BROKEN_LINKS: 238ee419e0eSNicolas GERARD $this->brokenLinks(); 239ee419e0eSNicolas GERARD break; 24071f916b9Sgerardnico case self::ANALYTICS: 241c3437056SNickeau $startPath = $this->getStartPath($args); 242007225e5Sgerardnico $output = $options->getOpt('output', ''); 243007225e5Sgerardnico //if ($output == '-') $output = 'php://stdout'; 244c3437056SNickeau $this->analytics($startPath, $output, $depth); 24571f916b9Sgerardnico break; 24671f916b9Sgerardnico case self::SYNC: 247*44e19ce3SNicolas GERARD // php "$DOKUWIKI_HOME"/bin/indexer.php -q 248c3437056SNickeau $this->deleteNonExistingPageFromDatabase(); 249c3437056SNickeau break; 250c3437056SNickeau case self::PLUGINS_TO_UPDATE: 251ee419e0eSNicolas GERARD $this->pluginToUpdate(); 25271f916b9Sgerardnico break; 25371f916b9Sgerardnico default: 254c3437056SNickeau if ($cmd !== "") { 255c3437056SNickeau fwrite(STDERR, "Combo: Command unknown (" . $cmd . ")"); 256c3437056SNickeau } else { 257c3437056SNickeau echo $options->help(); 258c3437056SNickeau } 259c3437056SNickeau exit(1); 26071f916b9Sgerardnico } 261ee419e0eSNicolas GERARD } catch (Exception $exception) { 262ee419e0eSNicolas GERARD fwrite(STDERR, "An internal error has occurred. " . $exception->getMessage() . "\n" . $exception->getTraceAsString()); 2634ebc3257Sgerardnico exit(1); 2644ebc3257Sgerardnico } 265007225e5Sgerardnico 266007225e5Sgerardnico 267007225e5Sgerardnico } 268007225e5Sgerardnico 269007225e5Sgerardnico /** 27071f916b9Sgerardnico * @param array $namespaces 271c3437056SNickeau * @param bool $rebuild 272007225e5Sgerardnico * @param int $depth recursion depth. 0 for unlimited 27304fd306cSNickeau * @throws ExceptionCompile 274007225e5Sgerardnico */ 275c3437056SNickeau private function index($namespaces = array(), $rebuild = false, $depth = 0) 276c3437056SNickeau { 277c3437056SNickeau 278c3437056SNickeau /** 279c3437056SNickeau * Run as admin to overcome the fact that 280c3437056SNickeau * anonymous user cannot see all links and backlinks 281c3437056SNickeau */ 282c3437056SNickeau global $USERINFO; 283c3437056SNickeau $USERINFO['grps'] = array('admin'); 284c3437056SNickeau global $INPUT; 285c3437056SNickeau $INPUT->server->set('REMOTE_USER', "cli"); 286c3437056SNickeau 287c3437056SNickeau $pages = FsWikiUtility::getPages($namespaces, $depth); 288c3437056SNickeau 289c3437056SNickeau $pageCounter = 0; 290c3437056SNickeau $totalNumberOfPages = sizeof($pages); 291c3437056SNickeau while ($pageArray = array_shift($pages)) { 292c3437056SNickeau $id = $pageArray['id']; 2934cadd4f8SNickeau global $ID; 2944cadd4f8SNickeau $ID = $id; 295c3437056SNickeau /** 296c3437056SNickeau * Indexing the page start the database replication 29704fd306cSNickeau * See {@link action_plugin_combo_indexer} 298c3437056SNickeau */ 299c3437056SNickeau $pageCounter++; 300edc35203Sgerardnico $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 301c3437056SNickeau try { 302c3437056SNickeau /** 303c3437056SNickeau * If the page does not need to be indexed, there is no run 304c3437056SNickeau * and false is returned 305c3437056SNickeau */ 306c3437056SNickeau $indexedOrNot = idx_addPage($id, true, true); 307c3437056SNickeau if ($indexedOrNot) { 308c3437056SNickeau LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) was indexed and replicated", LogUtility::LVL_MSG_INFO); 309c3437056SNickeau } else { 310c3437056SNickeau LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) has an error", LogUtility::LVL_MSG_ERROR); 311c3437056SNickeau } 31204fd306cSNickeau } catch (ExceptionRuntime $e) { 313c3437056SNickeau LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) has an error: " . $e->getMessage(), LogUtility::LVL_MSG_ERROR); 314edc35203Sgerardnico } finally { 315edc35203Sgerardnico $executionContext->close(); 316c3437056SNickeau } 317c3437056SNickeau } 318c3437056SNickeau /** 319c3437056SNickeau * Process all backlinks 320c3437056SNickeau */ 321c3437056SNickeau echo "Processing Replication Request\n"; 322c3437056SNickeau Event::dispatchEvent(PHP_INT_MAX); 323c3437056SNickeau 324c3437056SNickeau } 325c3437056SNickeau 326c3437056SNickeau private function analytics($namespaces = array(), $output = null, $depth = 0) 327007225e5Sgerardnico { 328007225e5Sgerardnico 329007225e5Sgerardnico $fileHandle = null; 330007225e5Sgerardnico if (!empty($output)) { 331007225e5Sgerardnico $fileHandle = @fopen($output, 'w'); 332007225e5Sgerardnico if (!$fileHandle) $this->fatal("Failed to open $output"); 333007225e5Sgerardnico } 334007225e5Sgerardnico 33537748cd8SNickeau /** 33637748cd8SNickeau * Run as admin to overcome the fact that 33737748cd8SNickeau * anonymous user cannot see all links and backlinks 33837748cd8SNickeau */ 33937748cd8SNickeau global $USERINFO; 34037748cd8SNickeau $USERINFO['grps'] = array('admin'); 34137748cd8SNickeau global $INPUT; 34237748cd8SNickeau $INPUT->server->set('REMOTE_USER', "cli"); 34337748cd8SNickeau 34437748cd8SNickeau $pages = FsWikiUtility::getPages($namespaces, $depth); 345007225e5Sgerardnico 346007225e5Sgerardnico 347007225e5Sgerardnico if (!empty($fileHandle)) { 348007225e5Sgerardnico $header = array( 349007225e5Sgerardnico 'id', 350007225e5Sgerardnico 'backlinks', 351007225e5Sgerardnico 'broken_links', 352007225e5Sgerardnico 'changes', 353007225e5Sgerardnico 'chars', 354007225e5Sgerardnico 'external_links', 355007225e5Sgerardnico 'external_medias', 356007225e5Sgerardnico 'h1', 357007225e5Sgerardnico 'h2', 358007225e5Sgerardnico 'h3', 359007225e5Sgerardnico 'h4', 360007225e5Sgerardnico 'h5', 361007225e5Sgerardnico 'internal_links', 362007225e5Sgerardnico 'internal_medias', 363007225e5Sgerardnico 'words', 364007225e5Sgerardnico 'score' 365007225e5Sgerardnico ); 366007225e5Sgerardnico fwrite($fileHandle, implode(",", $header) . PHP_EOL); 367007225e5Sgerardnico } 3689da76789Sgerardnico $pageCounter = 0; 369e8b2ff59SNickeau $totalNumberOfPages = sizeof($pages); 370c3437056SNickeau while ($pageArray = array_shift($pages)) { 371c3437056SNickeau $id = $pageArray['id']; 37204fd306cSNickeau $page = MarkupPath::createMarkupFromId($id); 373c3437056SNickeau 374007225e5Sgerardnico 3759da76789Sgerardnico $pageCounter++; 376c3437056SNickeau /** 377c3437056SNickeau * Analytics 378c3437056SNickeau */ 379edc35203Sgerardnico echo "Analytics Processing for the page {$id} ($pageCounter / $totalNumberOfPages)\n"; 380edc35203Sgerardnico $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 381edc35203Sgerardnico try { 38204fd306cSNickeau $analyticsPath = $page->fetchAnalyticsPath(); 383edc35203Sgerardnico } catch (ExceptionNotExists $e) { 384edc35203Sgerardnico LogUtility::error("The analytics document for the page ($page) was not found"); 385edc35203Sgerardnico continue; 386edc35203Sgerardnico } catch (ExceptionCompile $e) { 387edc35203Sgerardnico LogUtility::error("Error when get the analytics.", self::CANONICAL, $e); 388edc35203Sgerardnico continue; 389edc35203Sgerardnico } finally { 390edc35203Sgerardnico $executionContext->close(); 391edc35203Sgerardnico } 392edc35203Sgerardnico 39304fd306cSNickeau try { 39404fd306cSNickeau $data = \ComboStrap\Json::createFromPath($analyticsPath)->toArray(); 39504fd306cSNickeau } catch (ExceptionBadSyntax $e) { 39604fd306cSNickeau LogUtility::error("The analytics json of the page ($page) is not conform"); 39704fd306cSNickeau continue; 398edc35203Sgerardnico } catch (ExceptionNotFound|ExceptionNotExists $e) { 39904fd306cSNickeau LogUtility::error("The analytics document ({$analyticsPath}) for the page ($page) was not found"); 40004fd306cSNickeau continue; 40104fd306cSNickeau } 402c3437056SNickeau 403007225e5Sgerardnico if (!empty($fileHandle)) { 40404fd306cSNickeau $statistics = $data[renderer_plugin_combo_analytics::STATISTICS]; 405007225e5Sgerardnico $row = array( 406007225e5Sgerardnico 'id' => $id, 407c3437056SNickeau 'backlinks' => $statistics[BacklinkCount::getPersistentName()], 40804fd306cSNickeau 'broken_links' => $statistics[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT], 40904fd306cSNickeau 'changes' => $statistics[renderer_plugin_combo_analytics::EDITS_COUNT], 41004fd306cSNickeau 'chars' => $statistics[renderer_plugin_combo_analytics::CHAR_COUNT], 41104fd306cSNickeau 'external_links' => $statistics[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT], 41204fd306cSNickeau 'external_medias' => $statistics[renderer_plugin_combo_analytics::EXTERNAL_MEDIA_COUNT], 41304fd306cSNickeau PageH1::PROPERTY_NAME => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT][PageH1::PROPERTY_NAME], 41404fd306cSNickeau 'h2' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h2'], 41504fd306cSNickeau 'h3' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h3'], 41604fd306cSNickeau 'h4' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h4'], 41704fd306cSNickeau 'h5' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h5'], 41804fd306cSNickeau 'internal_links' => $statistics[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT], 41904fd306cSNickeau 'internal_medias' => $statistics[renderer_plugin_combo_analytics::INTERNAL_MEDIA_COUNT], 42004fd306cSNickeau 'words' => $statistics[renderer_plugin_combo_analytics::WORD_COUNT], 42104fd306cSNickeau 'low' => $data[renderer_plugin_combo_analytics::QUALITY]['low'] 422007225e5Sgerardnico ); 423007225e5Sgerardnico fwrite($fileHandle, implode(",", $row) . PHP_EOL); 424007225e5Sgerardnico } 425c3437056SNickeau 426007225e5Sgerardnico } 427007225e5Sgerardnico if (!empty($fileHandle)) { 428007225e5Sgerardnico fclose($fileHandle); 429007225e5Sgerardnico } 430007225e5Sgerardnico 431007225e5Sgerardnico } 43271f916b9Sgerardnico 433325fe0c5Sgerardnico 4344ebc3257Sgerardnico /** 435ee419e0eSNicolas GERARD * @throws ExceptionSqliteNotAvailable 4364ebc3257Sgerardnico */ 437c3437056SNickeau private function deleteNonExistingPageFromDatabase() 43871f916b9Sgerardnico { 439c3437056SNickeau LogUtility::msg("Starting: Deleting non-existing page from database"); 440c3437056SNickeau $sqlite = Sqlite::createOrGetSqlite(); 441ee419e0eSNicolas GERARD /** @noinspection SqlNoDataSourceInspection */ 442c3437056SNickeau $request = $sqlite 443c3437056SNickeau ->createRequest() 444c3437056SNickeau ->setQuery("select id as \"id\" from pages"); 445c3437056SNickeau $rows = []; 446c3437056SNickeau try { 447c3437056SNickeau $rows = $request 448c3437056SNickeau ->execute() 449c3437056SNickeau ->getRows(); 45004fd306cSNickeau } catch (ExceptionCompile $e) { 451c3437056SNickeau LogUtility::msg("Error while getting the id pages. {$e->getMessage()}"); 452c3437056SNickeau return; 453c3437056SNickeau } finally { 454c3437056SNickeau $request->close(); 45571f916b9Sgerardnico } 456c3437056SNickeau $counter = 0; 457031d4b49Sgerardnico 458c3437056SNickeau foreach ($rows as $row) { 459031d4b49Sgerardnico /** 460031d4b49Sgerardnico * Context 461031d4b49Sgerardnico * PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 20480 bytes) 462031d4b49Sgerardnico * in /opt/www/datacadamia.com/inc/ErrorHandler.php on line 102 463031d4b49Sgerardnico */ 464031d4b49Sgerardnico $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 465031d4b49Sgerardnico try { 466c3437056SNickeau $counter++; 467c3437056SNickeau $id = $row['id']; 46871f916b9Sgerardnico if (!page_exists($id)) { 4694ebc3257Sgerardnico echo 'Page does not exist on the file system. Delete from the database (' . $id . ")\n"; 4704ebc3257Sgerardnico try { 471031d4b49Sgerardnico $dbRow = DatabasePageRow::getFromDokuWikiId($id); 472031d4b49Sgerardnico $dbRow->delete(); 4734ebc3257Sgerardnico } catch (ExceptionNotFound $e) { 474031d4b49Sgerardnico // ok 4754ebc3257Sgerardnico } 476c3437056SNickeau } 477031d4b49Sgerardnico } finally { 478031d4b49Sgerardnico $executionContext->close(); 479031d4b49Sgerardnico } 480031d4b49Sgerardnico 481c3437056SNickeau } 482c3437056SNickeau LogUtility::msg("Sync finished ($counter pages checked)"); 483c3437056SNickeau 484c3437056SNickeau } 485c3437056SNickeau 486c3437056SNickeau private function frontmatter($namespaces, $depth) 487c3437056SNickeau { 488c3437056SNickeau $pages = FsWikiUtility::getPages($namespaces, $depth); 489c3437056SNickeau $pageCounter = 0; 490c3437056SNickeau $totalNumberOfPages = sizeof($pages); 491c3437056SNickeau $pagesWithChanges = []; 492c3437056SNickeau $pagesWithError = []; 493c3437056SNickeau $pagesWithOthers = []; 494c3437056SNickeau $notChangedCounter = 0; 495c3437056SNickeau while ($pageArray = array_shift($pages)) { 496c3437056SNickeau $id = $pageArray['id']; 4974cadd4f8SNickeau global $ID; 4984cadd4f8SNickeau $ID = $id; 49904fd306cSNickeau $page = MarkupPath::createMarkupFromId($id); 500c3437056SNickeau $pageCounter++; 501ee419e0eSNicolas GERARD LogUtility::msg("Processing page $id ($pageCounter / $totalNumberOfPages) ", LogUtility::LVL_MSG_INFO); 502edc35203Sgerardnico $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 503c3437056SNickeau try { 504c3437056SNickeau $message = MetadataFrontmatterStore::createFromPage($page) 505c3437056SNickeau ->sync(); 506c3437056SNickeau switch ($message->getStatus()) { 507c3437056SNickeau case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_CHANGED: 508c3437056SNickeau $notChangedCounter++; 509c3437056SNickeau break; 510c3437056SNickeau case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_DONE: 511c3437056SNickeau $pagesWithChanges[] = $id; 512c3437056SNickeau break; 513c3437056SNickeau case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_ERROR: 514c3437056SNickeau $pagesWithError[$id] = $message->getPlainTextContent(); 515c3437056SNickeau break; 516c3437056SNickeau default: 517c3437056SNickeau $pagesWithOthers[$id] = $message->getPlainTextContent(); 518c3437056SNickeau break; 519c3437056SNickeau 520c3437056SNickeau } 52104fd306cSNickeau } catch (ExceptionCompile $e) { 522c3437056SNickeau $pagesWithError[$id] = $e->getMessage(); 523edc35203Sgerardnico } finally { 524edc35203Sgerardnico $executionContext->close(); 525c3437056SNickeau } 526c3437056SNickeau 527c3437056SNickeau } 528c3437056SNickeau 529c3437056SNickeau echo "\n"; 530c3437056SNickeau echo "Result:\n"; 531c3437056SNickeau echo "$notChangedCounter pages without any frontmatter modifications\n"; 532c3437056SNickeau 533c3437056SNickeau if (sizeof($pagesWithError) > 0) { 534c3437056SNickeau echo "\n"; 535c3437056SNickeau echo "The following pages had errors\n"; 536c3437056SNickeau $pageCounter = 0; 537c3437056SNickeau $totalNumberOfPages = sizeof($pagesWithError); 538c3437056SNickeau foreach ($pagesWithError as $id => $message) { 539c3437056SNickeau $pageCounter++; 540ee419e0eSNicolas GERARD LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages): " . $message); 541c3437056SNickeau } 542c3437056SNickeau } else { 543c3437056SNickeau echo "No error\n"; 544c3437056SNickeau } 545c3437056SNickeau 546c3437056SNickeau if (sizeof($pagesWithChanges) > 0) { 547c3437056SNickeau echo "\n"; 548c3437056SNickeau echo "The following pages had changed:\n"; 549c3437056SNickeau $pageCounter = 0; 550c3437056SNickeau $totalNumberOfPages = sizeof($pagesWithChanges); 551c3437056SNickeau foreach ($pagesWithChanges as $id) { 552c3437056SNickeau $pageCounter++; 553ee419e0eSNicolas GERARD LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages) "); 554c3437056SNickeau } 555c3437056SNickeau } else { 556c3437056SNickeau echo "No changes\n"; 557c3437056SNickeau } 558c3437056SNickeau 559c3437056SNickeau if (sizeof($pagesWithOthers) > 0) { 560c3437056SNickeau echo "\n"; 561c3437056SNickeau echo "The following pages had an other status"; 562c3437056SNickeau $pageCounter = 0; 563c3437056SNickeau $totalNumberOfPages = sizeof($pagesWithOthers); 564c3437056SNickeau foreach ($pagesWithOthers as $id => $message) { 565c3437056SNickeau $pageCounter++; 566ee419e0eSNicolas GERARD LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages) " . $message, LogUtility::LVL_MSG_ERROR); 567c3437056SNickeau } 56871f916b9Sgerardnico } 56971f916b9Sgerardnico } 57071f916b9Sgerardnico 571c3437056SNickeau private function getStartPath($args) 572c3437056SNickeau { 573c3437056SNickeau $sizeof = sizeof($args); 574c3437056SNickeau switch ($sizeof) { 575c3437056SNickeau case 0: 576c3437056SNickeau fwrite(STDERR, "The start path is mandatory and was not given"); 577c3437056SNickeau exit(1); 578c3437056SNickeau case 1: 579c3437056SNickeau $startPath = $args[0]; 580c3437056SNickeau if (!in_array($startPath, [":", "/"])) { 581c3437056SNickeau // cleanId would return blank for a root 582c3437056SNickeau $startPath = cleanID($startPath); 583c3437056SNickeau } 584c3437056SNickeau break; 585c3437056SNickeau default: 586c3437056SNickeau fwrite(STDERR, "Too much arguments given $sizeof"); 587c3437056SNickeau exit(1); 588c3437056SNickeau } 589c3437056SNickeau return $startPath; 59071f916b9Sgerardnico } 591ee419e0eSNicolas GERARD 592ee419e0eSNicolas GERARD /** 593ee419e0eSNicolas GERARD * 594ee419e0eSNicolas GERARD * Print the extension/plugin to update 595ee419e0eSNicolas GERARD * 596ee419e0eSNicolas GERARD * Note, there is also an Endpoint: 597ee419e0eSNicolas GERARD * self::EXTENSION_REPOSITORY_API.'?fmt=php&ext[]='.urlencode($name) 598ee419e0eSNicolas GERARD * `http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php?fmt=php&ext[]=`.urlencode($name) 599ee419e0eSNicolas GERARD * 600ee419e0eSNicolas GERARD * @noinspection PhpUndefinedClassInspection 601ee419e0eSNicolas GERARD */ 602ee419e0eSNicolas GERARD private function pluginToUpdate() 603ee419e0eSNicolas GERARD { 604ee419e0eSNicolas GERARD 605ee419e0eSNicolas GERARD if (class_exists(Local::class)) { 606ee419e0eSNicolas GERARD /** 607ee419e0eSNicolas GERARD * Release 2025-05-14 "Librarian" 608ee419e0eSNicolas GERARD * https://www.dokuwiki.org/changes#release_2025-05-14_librarian 609ee419e0eSNicolas GERARD * https://www.patreon.com/posts/new-extension-116501986 610ee419e0eSNicolas GERARD * ./bin/plugin.php extension list 611ee419e0eSNicolas GERARD * @link lib/plugins/extension/cli.php 612ee419e0eSNicolas GERARD * Code based on https://github.com/giterlizzi/dokuwiki-template-bootstrap3/pull/617/files 613ee419e0eSNicolas GERARD */ 614ee419e0eSNicolas GERARD try { 615ee419e0eSNicolas GERARD $extensions = (new Local())->getExtensions(); 616ee419e0eSNicolas GERARD Repository::getInstance()->initExtensions(array_keys($extensions)); 617ee419e0eSNicolas GERARD foreach ($extensions as $extension) { 618ee419e0eSNicolas GERARD if ($extension->isEnabled() && $extension->isUpdateAvailable()) { 619ee419e0eSNicolas GERARD echo "The extension {$extension->getDisplayName()} should be updated"; 620ee419e0eSNicolas GERARD } 621ee419e0eSNicolas GERARD } 622ee419e0eSNicolas GERARD } /** @noinspection PhpUndefinedClassInspection */ catch (ExtensionException $ignore) { 623ee419e0eSNicolas GERARD // Ignore the exception 624ee419e0eSNicolas GERARD } 625ee419e0eSNicolas GERARD return; 626ee419e0eSNicolas GERARD } 627ee419e0eSNicolas GERARD 628ee419e0eSNicolas GERARD 629ee419e0eSNicolas GERARD $pluginList = plugin_list('', true); 630ee419e0eSNicolas GERARD $extension = $this->loadHelper('extension_extension'); 631ee419e0eSNicolas GERARD foreach ($pluginList as $name) { 632ee419e0eSNicolas GERARD 633ee419e0eSNicolas GERARD /* @var helper_plugin_extension_extension $extension 634ee419e0eSNicolas GERARD * old extension manager until Kaos 635ee419e0eSNicolas GERARD */ 636ee419e0eSNicolas GERARD $extension->setExtension($name); 637ee419e0eSNicolas GERARD /** @noinspection PhpUndefinedMethodInspection */ 638ee419e0eSNicolas GERARD if ($extension->updateAvailable()) { 639ee419e0eSNicolas GERARD echo "The extension $name should be updated"; 640ee419e0eSNicolas GERARD } 641ee419e0eSNicolas GERARD } 642ee419e0eSNicolas GERARD 643ee419e0eSNicolas GERARD 644ee419e0eSNicolas GERARD } 645ee419e0eSNicolas GERARD 646ee419e0eSNicolas GERARD /** 647ee419e0eSNicolas GERARD * @return void 648ee419e0eSNicolas GERARD * Print the broken Links 649ee419e0eSNicolas GERARD * @throws ExceptionSqliteNotAvailable 650ee419e0eSNicolas GERARD */ 651ee419e0eSNicolas GERARD private function brokenLinks() 652ee419e0eSNicolas GERARD { 653ee419e0eSNicolas GERARD LogUtility::msg("Broken Links Started"); 654ee419e0eSNicolas GERARD $sqlite = Sqlite::createOrGetSqlite(); 655ee419e0eSNicolas GERARD $request = $sqlite 656ee419e0eSNicolas GERARD ->createRequest() 657ee419e0eSNicolas GERARD ->setQuery("with validPages as (select path, analytics 658ee419e0eSNicolas GERARD from pages 659ee419e0eSNicolas GERARD where json_valid(analytics) = 1) 660ee419e0eSNicolas GERARDselect path, 661ee419e0eSNicolas GERARD json_extract(analytics, '$.statistics.internal_broken_link_count') as broken_link, 662ee419e0eSNicolas GERARD json_extract(analytics, '$.statistics.media.internal_broken_count') as broken_media 663ee419e0eSNicolas GERARDfrom validPages 664ee419e0eSNicolas GERARDwhere json_extract(analytics, '$.statistics.internal_broken_link_count') is not null 665ee419e0eSNicolas GERARD or json_extract(analytics, '$.statistics.media.internal_broken_count') != 0"); 666ee419e0eSNicolas GERARD $rows = []; 667ee419e0eSNicolas GERARD try { 668ee419e0eSNicolas GERARD $rows = $request 669ee419e0eSNicolas GERARD ->execute() 670ee419e0eSNicolas GERARD ->getRows(); 671ee419e0eSNicolas GERARD } catch (ExceptionCompile $e) { 672ee419e0eSNicolas GERARD LogUtility::msg("Error while getting the id pages. {$e->getMessage()}"); 673ee419e0eSNicolas GERARD return; 674ee419e0eSNicolas GERARD } finally { 675ee419e0eSNicolas GERARD $request->close(); 676ee419e0eSNicolas GERARD } 677328b625dSNicolas GERARD if (count($rows) == 0) { 678328b625dSNicolas GERARD LogUtility::msg("No Broken Links"); 679328b625dSNicolas GERARD exit(); 680328b625dSNicolas GERARD } 681ee419e0eSNicolas GERARD LogUtility::msg("Broken Links:"); 682ee419e0eSNicolas GERARD foreach ($rows as $row) { 683ee419e0eSNicolas GERARD $path = $row["path"]; 684ee419e0eSNicolas GERARD $broken_link = $row["broken_link"]; 685ee419e0eSNicolas GERARD $broken_media = $row["broken_media"]; 686ee419e0eSNicolas GERARD echo "$path (Page: $broken_link, Media: $broken_media) \n"; 687ee419e0eSNicolas GERARD } 688ee419e0eSNicolas GERARD if (count($rows) != 0) { 689ee419e0eSNicolas GERARD exit(1); 690ee419e0eSNicolas GERARD } 691ee419e0eSNicolas GERARD } 692007225e5Sgerardnico} 693