xref: /plugin/combo/cli.php (revision 328b625d295860658952af40bd37dab2cf646c0b)
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:
247c3437056SNickeau                    $this->deleteNonExistingPageFromDatabase();
248c3437056SNickeau                    break;
249c3437056SNickeau                case self::PLUGINS_TO_UPDATE:
250ee419e0eSNicolas GERARD                    $this->pluginToUpdate();
25171f916b9Sgerardnico                    break;
25271f916b9Sgerardnico                default:
253c3437056SNickeau                    if ($cmd !== "") {
254c3437056SNickeau                        fwrite(STDERR, "Combo: Command unknown (" . $cmd . ")");
255c3437056SNickeau                    } else {
256c3437056SNickeau                        echo $options->help();
257c3437056SNickeau                    }
258c3437056SNickeau                    exit(1);
25971f916b9Sgerardnico            }
260ee419e0eSNicolas GERARD        } catch (Exception $exception) {
261ee419e0eSNicolas GERARD            fwrite(STDERR, "An internal error has occurred. " . $exception->getMessage() . "\n" . $exception->getTraceAsString());
2624ebc3257Sgerardnico            exit(1);
2634ebc3257Sgerardnico        }
264007225e5Sgerardnico
265007225e5Sgerardnico
266007225e5Sgerardnico    }
267007225e5Sgerardnico
268007225e5Sgerardnico    /**
26971f916b9Sgerardnico     * @param array $namespaces
270c3437056SNickeau     * @param bool $rebuild
271007225e5Sgerardnico     * @param int $depth recursion depth. 0 for unlimited
27204fd306cSNickeau     * @throws ExceptionCompile
273007225e5Sgerardnico     */
274c3437056SNickeau    private function index($namespaces = array(), $rebuild = false, $depth = 0)
275c3437056SNickeau    {
276c3437056SNickeau
277c3437056SNickeau        /**
278c3437056SNickeau         * Run as admin to overcome the fact that
279c3437056SNickeau         * anonymous user cannot see all links and backlinks
280c3437056SNickeau         */
281c3437056SNickeau        global $USERINFO;
282c3437056SNickeau        $USERINFO['grps'] = array('admin');
283c3437056SNickeau        global $INPUT;
284c3437056SNickeau        $INPUT->server->set('REMOTE_USER', "cli");
285c3437056SNickeau
286c3437056SNickeau        $pages = FsWikiUtility::getPages($namespaces, $depth);
287c3437056SNickeau
288c3437056SNickeau        $pageCounter = 0;
289c3437056SNickeau        $totalNumberOfPages = sizeof($pages);
290c3437056SNickeau        while ($pageArray = array_shift($pages)) {
291c3437056SNickeau            $id = $pageArray['id'];
2924cadd4f8SNickeau            global $ID;
2934cadd4f8SNickeau            $ID = $id;
294c3437056SNickeau            /**
295c3437056SNickeau             * Indexing the page start the database replication
29604fd306cSNickeau             * See {@link action_plugin_combo_indexer}
297c3437056SNickeau             */
298c3437056SNickeau            $pageCounter++;
299edc35203Sgerardnico            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
300c3437056SNickeau            try {
301c3437056SNickeau                /**
302c3437056SNickeau                 * If the page does not need to be indexed, there is no run
303c3437056SNickeau                 * and false is returned
304c3437056SNickeau                 */
305c3437056SNickeau                $indexedOrNot = idx_addPage($id, true, true);
306c3437056SNickeau                if ($indexedOrNot) {
307c3437056SNickeau                    LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) was indexed and replicated", LogUtility::LVL_MSG_INFO);
308c3437056SNickeau                } else {
309c3437056SNickeau                    LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) has an error", LogUtility::LVL_MSG_ERROR);
310c3437056SNickeau                }
31104fd306cSNickeau            } catch (ExceptionRuntime $e) {
312c3437056SNickeau                LogUtility::msg("The page {$id} ($pageCounter / $totalNumberOfPages) has an error: " . $e->getMessage(), LogUtility::LVL_MSG_ERROR);
313edc35203Sgerardnico            } finally {
314edc35203Sgerardnico                $executionContext->close();
315c3437056SNickeau            }
316c3437056SNickeau        }
317c3437056SNickeau        /**
318c3437056SNickeau         * Process all backlinks
319c3437056SNickeau         */
320c3437056SNickeau        echo "Processing Replication Request\n";
321c3437056SNickeau        Event::dispatchEvent(PHP_INT_MAX);
322c3437056SNickeau
323c3437056SNickeau    }
324c3437056SNickeau
325c3437056SNickeau    private function analytics($namespaces = array(), $output = null, $depth = 0)
326007225e5Sgerardnico    {
327007225e5Sgerardnico
328007225e5Sgerardnico        $fileHandle = null;
329007225e5Sgerardnico        if (!empty($output)) {
330007225e5Sgerardnico            $fileHandle = @fopen($output, 'w');
331007225e5Sgerardnico            if (!$fileHandle) $this->fatal("Failed to open $output");
332007225e5Sgerardnico        }
333007225e5Sgerardnico
33437748cd8SNickeau        /**
33537748cd8SNickeau         * Run as admin to overcome the fact that
33637748cd8SNickeau         * anonymous user cannot see all links and backlinks
33737748cd8SNickeau         */
33837748cd8SNickeau        global $USERINFO;
33937748cd8SNickeau        $USERINFO['grps'] = array('admin');
34037748cd8SNickeau        global $INPUT;
34137748cd8SNickeau        $INPUT->server->set('REMOTE_USER', "cli");
34237748cd8SNickeau
34337748cd8SNickeau        $pages = FsWikiUtility::getPages($namespaces, $depth);
344007225e5Sgerardnico
345007225e5Sgerardnico
346007225e5Sgerardnico        if (!empty($fileHandle)) {
347007225e5Sgerardnico            $header = array(
348007225e5Sgerardnico                'id',
349007225e5Sgerardnico                'backlinks',
350007225e5Sgerardnico                'broken_links',
351007225e5Sgerardnico                'changes',
352007225e5Sgerardnico                'chars',
353007225e5Sgerardnico                'external_links',
354007225e5Sgerardnico                'external_medias',
355007225e5Sgerardnico                'h1',
356007225e5Sgerardnico                'h2',
357007225e5Sgerardnico                'h3',
358007225e5Sgerardnico                'h4',
359007225e5Sgerardnico                'h5',
360007225e5Sgerardnico                'internal_links',
361007225e5Sgerardnico                'internal_medias',
362007225e5Sgerardnico                'words',
363007225e5Sgerardnico                'score'
364007225e5Sgerardnico            );
365007225e5Sgerardnico            fwrite($fileHandle, implode(",", $header) . PHP_EOL);
366007225e5Sgerardnico        }
3679da76789Sgerardnico        $pageCounter = 0;
368e8b2ff59SNickeau        $totalNumberOfPages = sizeof($pages);
369c3437056SNickeau        while ($pageArray = array_shift($pages)) {
370c3437056SNickeau            $id = $pageArray['id'];
37104fd306cSNickeau            $page = MarkupPath::createMarkupFromId($id);
372c3437056SNickeau
373007225e5Sgerardnico
3749da76789Sgerardnico            $pageCounter++;
375c3437056SNickeau            /**
376c3437056SNickeau             * Analytics
377c3437056SNickeau             */
378edc35203Sgerardnico            echo "Analytics Processing for the page {$id} ($pageCounter / $totalNumberOfPages)\n";
379edc35203Sgerardnico            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
380edc35203Sgerardnico            try {
38104fd306cSNickeau                $analyticsPath = $page->fetchAnalyticsPath();
382edc35203Sgerardnico            } catch (ExceptionNotExists $e) {
383edc35203Sgerardnico                LogUtility::error("The analytics document for the page ($page) was not found");
384edc35203Sgerardnico                continue;
385edc35203Sgerardnico            } catch (ExceptionCompile $e) {
386edc35203Sgerardnico                LogUtility::error("Error when get the analytics.", self::CANONICAL, $e);
387edc35203Sgerardnico                continue;
388edc35203Sgerardnico            } finally {
389edc35203Sgerardnico                $executionContext->close();
390edc35203Sgerardnico            }
391edc35203Sgerardnico
39204fd306cSNickeau            try {
39304fd306cSNickeau                $data = \ComboStrap\Json::createFromPath($analyticsPath)->toArray();
39404fd306cSNickeau            } catch (ExceptionBadSyntax $e) {
39504fd306cSNickeau                LogUtility::error("The analytics json of the page ($page) is not conform");
39604fd306cSNickeau                continue;
397edc35203Sgerardnico            } catch (ExceptionNotFound|ExceptionNotExists $e) {
39804fd306cSNickeau                LogUtility::error("The analytics document ({$analyticsPath}) for the page ($page) was not found");
39904fd306cSNickeau                continue;
40004fd306cSNickeau            }
401c3437056SNickeau
402007225e5Sgerardnico            if (!empty($fileHandle)) {
40304fd306cSNickeau                $statistics = $data[renderer_plugin_combo_analytics::STATISTICS];
404007225e5Sgerardnico                $row = array(
405007225e5Sgerardnico                    'id' => $id,
406c3437056SNickeau                    'backlinks' => $statistics[BacklinkCount::getPersistentName()],
40704fd306cSNickeau                    'broken_links' => $statistics[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT],
40804fd306cSNickeau                    'changes' => $statistics[renderer_plugin_combo_analytics::EDITS_COUNT],
40904fd306cSNickeau                    'chars' => $statistics[renderer_plugin_combo_analytics::CHAR_COUNT],
41004fd306cSNickeau                    'external_links' => $statistics[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT],
41104fd306cSNickeau                    'external_medias' => $statistics[renderer_plugin_combo_analytics::EXTERNAL_MEDIA_COUNT],
41204fd306cSNickeau                    PageH1::PROPERTY_NAME => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT][PageH1::PROPERTY_NAME],
41304fd306cSNickeau                    'h2' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h2'],
41404fd306cSNickeau                    'h3' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h3'],
41504fd306cSNickeau                    'h4' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h4'],
41604fd306cSNickeau                    'h5' => $statistics[renderer_plugin_combo_analytics::HEADING_COUNT]['h5'],
41704fd306cSNickeau                    'internal_links' => $statistics[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT],
41804fd306cSNickeau                    'internal_medias' => $statistics[renderer_plugin_combo_analytics::INTERNAL_MEDIA_COUNT],
41904fd306cSNickeau                    'words' => $statistics[renderer_plugin_combo_analytics::WORD_COUNT],
42004fd306cSNickeau                    'low' => $data[renderer_plugin_combo_analytics::QUALITY]['low']
421007225e5Sgerardnico                );
422007225e5Sgerardnico                fwrite($fileHandle, implode(",", $row) . PHP_EOL);
423007225e5Sgerardnico            }
424c3437056SNickeau
425007225e5Sgerardnico        }
426007225e5Sgerardnico        if (!empty($fileHandle)) {
427007225e5Sgerardnico            fclose($fileHandle);
428007225e5Sgerardnico        }
429007225e5Sgerardnico
430007225e5Sgerardnico    }
43171f916b9Sgerardnico
432325fe0c5Sgerardnico
4334ebc3257Sgerardnico    /**
434ee419e0eSNicolas GERARD     * @throws ExceptionSqliteNotAvailable
4354ebc3257Sgerardnico     */
436c3437056SNickeau    private function deleteNonExistingPageFromDatabase()
43771f916b9Sgerardnico    {
438c3437056SNickeau        LogUtility::msg("Starting: Deleting non-existing page from database");
439c3437056SNickeau        $sqlite = Sqlite::createOrGetSqlite();
440ee419e0eSNicolas GERARD        /** @noinspection SqlNoDataSourceInspection */
441c3437056SNickeau        $request = $sqlite
442c3437056SNickeau            ->createRequest()
443c3437056SNickeau            ->setQuery("select id as \"id\" from pages");
444c3437056SNickeau        $rows = [];
445c3437056SNickeau        try {
446c3437056SNickeau            $rows = $request
447c3437056SNickeau                ->execute()
448c3437056SNickeau                ->getRows();
44904fd306cSNickeau        } catch (ExceptionCompile $e) {
450c3437056SNickeau            LogUtility::msg("Error while getting the id pages. {$e->getMessage()}");
451c3437056SNickeau            return;
452c3437056SNickeau        } finally {
453c3437056SNickeau            $request->close();
45471f916b9Sgerardnico        }
455c3437056SNickeau        $counter = 0;
456031d4b49Sgerardnico
457c3437056SNickeau        foreach ($rows as $row) {
458031d4b49Sgerardnico            /**
459031d4b49Sgerardnico             * Context
460031d4b49Sgerardnico             * PHP Fatal error:  Allowed memory size of 268435456 bytes exhausted (tried to allocate 20480 bytes)
461031d4b49Sgerardnico             * in /opt/www/datacadamia.com/inc/ErrorHandler.php on line 102
462031d4b49Sgerardnico             */
463031d4b49Sgerardnico            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
464031d4b49Sgerardnico            try {
465c3437056SNickeau                $counter++;
466c3437056SNickeau                $id = $row['id'];
46771f916b9Sgerardnico                if (!page_exists($id)) {
4684ebc3257Sgerardnico                    echo 'Page does not exist on the file system. Delete from the database (' . $id . ")\n";
4694ebc3257Sgerardnico                    try {
470031d4b49Sgerardnico                        $dbRow = DatabasePageRow::getFromDokuWikiId($id);
471031d4b49Sgerardnico                        $dbRow->delete();
4724ebc3257Sgerardnico                    } catch (ExceptionNotFound $e) {
473031d4b49Sgerardnico                        // ok
4744ebc3257Sgerardnico                    }
475c3437056SNickeau                }
476031d4b49Sgerardnico            } finally {
477031d4b49Sgerardnico                $executionContext->close();
478031d4b49Sgerardnico            }
479031d4b49Sgerardnico
480c3437056SNickeau        }
481c3437056SNickeau        LogUtility::msg("Sync finished ($counter pages checked)");
482c3437056SNickeau
483c3437056SNickeau    }
484c3437056SNickeau
485c3437056SNickeau    private function frontmatter($namespaces, $depth)
486c3437056SNickeau    {
487c3437056SNickeau        $pages = FsWikiUtility::getPages($namespaces, $depth);
488c3437056SNickeau        $pageCounter = 0;
489c3437056SNickeau        $totalNumberOfPages = sizeof($pages);
490c3437056SNickeau        $pagesWithChanges = [];
491c3437056SNickeau        $pagesWithError = [];
492c3437056SNickeau        $pagesWithOthers = [];
493c3437056SNickeau        $notChangedCounter = 0;
494c3437056SNickeau        while ($pageArray = array_shift($pages)) {
495c3437056SNickeau            $id = $pageArray['id'];
4964cadd4f8SNickeau            global $ID;
4974cadd4f8SNickeau            $ID = $id;
49804fd306cSNickeau            $page = MarkupPath::createMarkupFromId($id);
499c3437056SNickeau            $pageCounter++;
500ee419e0eSNicolas GERARD            LogUtility::msg("Processing page $id ($pageCounter / $totalNumberOfPages) ", LogUtility::LVL_MSG_INFO);
501edc35203Sgerardnico            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
502c3437056SNickeau            try {
503c3437056SNickeau                $message = MetadataFrontmatterStore::createFromPage($page)
504c3437056SNickeau                    ->sync();
505c3437056SNickeau                switch ($message->getStatus()) {
506c3437056SNickeau                    case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_NOT_CHANGED:
507c3437056SNickeau                        $notChangedCounter++;
508c3437056SNickeau                        break;
509c3437056SNickeau                    case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_DONE:
510c3437056SNickeau                        $pagesWithChanges[] = $id;
511c3437056SNickeau                        break;
512c3437056SNickeau                    case syntax_plugin_combo_frontmatter::UPDATE_EXIT_CODE_ERROR:
513c3437056SNickeau                        $pagesWithError[$id] = $message->getPlainTextContent();
514c3437056SNickeau                        break;
515c3437056SNickeau                    default:
516c3437056SNickeau                        $pagesWithOthers[$id] = $message->getPlainTextContent();
517c3437056SNickeau                        break;
518c3437056SNickeau
519c3437056SNickeau                }
52004fd306cSNickeau            } catch (ExceptionCompile $e) {
521c3437056SNickeau                $pagesWithError[$id] = $e->getMessage();
522edc35203Sgerardnico            } finally {
523edc35203Sgerardnico                $executionContext->close();
524c3437056SNickeau            }
525c3437056SNickeau
526c3437056SNickeau        }
527c3437056SNickeau
528c3437056SNickeau        echo "\n";
529c3437056SNickeau        echo "Result:\n";
530c3437056SNickeau        echo "$notChangedCounter pages without any frontmatter modifications\n";
531c3437056SNickeau
532c3437056SNickeau        if (sizeof($pagesWithError) > 0) {
533c3437056SNickeau            echo "\n";
534c3437056SNickeau            echo "The following pages had errors\n";
535c3437056SNickeau            $pageCounter = 0;
536c3437056SNickeau            $totalNumberOfPages = sizeof($pagesWithError);
537c3437056SNickeau            foreach ($pagesWithError as $id => $message) {
538c3437056SNickeau                $pageCounter++;
539ee419e0eSNicolas GERARD                LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages): " . $message);
540c3437056SNickeau            }
541c3437056SNickeau        } else {
542c3437056SNickeau            echo "No error\n";
543c3437056SNickeau        }
544c3437056SNickeau
545c3437056SNickeau        if (sizeof($pagesWithChanges) > 0) {
546c3437056SNickeau            echo "\n";
547c3437056SNickeau            echo "The following pages had changed:\n";
548c3437056SNickeau            $pageCounter = 0;
549c3437056SNickeau            $totalNumberOfPages = sizeof($pagesWithChanges);
550c3437056SNickeau            foreach ($pagesWithChanges as $id) {
551c3437056SNickeau                $pageCounter++;
552ee419e0eSNicolas GERARD                LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages) ");
553c3437056SNickeau            }
554c3437056SNickeau        } else {
555c3437056SNickeau            echo "No changes\n";
556c3437056SNickeau        }
557c3437056SNickeau
558c3437056SNickeau        if (sizeof($pagesWithOthers) > 0) {
559c3437056SNickeau            echo "\n";
560c3437056SNickeau            echo "The following pages had an other status";
561c3437056SNickeau            $pageCounter = 0;
562c3437056SNickeau            $totalNumberOfPages = sizeof($pagesWithOthers);
563c3437056SNickeau            foreach ($pagesWithOthers as $id => $message) {
564c3437056SNickeau                $pageCounter++;
565ee419e0eSNicolas GERARD                LogUtility::msg("Page $id ($pageCounter / $totalNumberOfPages) " . $message, LogUtility::LVL_MSG_ERROR);
566c3437056SNickeau            }
56771f916b9Sgerardnico        }
56871f916b9Sgerardnico    }
56971f916b9Sgerardnico
570c3437056SNickeau    private function getStartPath($args)
571c3437056SNickeau    {
572c3437056SNickeau        $sizeof = sizeof($args);
573c3437056SNickeau        switch ($sizeof) {
574c3437056SNickeau            case 0:
575c3437056SNickeau                fwrite(STDERR, "The start path is mandatory and was not given");
576c3437056SNickeau                exit(1);
577c3437056SNickeau            case 1:
578c3437056SNickeau                $startPath = $args[0];
579c3437056SNickeau                if (!in_array($startPath, [":", "/"])) {
580c3437056SNickeau                    // cleanId would return blank for a root
581c3437056SNickeau                    $startPath = cleanID($startPath);
582c3437056SNickeau                }
583c3437056SNickeau                break;
584c3437056SNickeau            default:
585c3437056SNickeau                fwrite(STDERR, "Too much arguments given $sizeof");
586c3437056SNickeau                exit(1);
587c3437056SNickeau        }
588c3437056SNickeau        return $startPath;
58971f916b9Sgerardnico    }
590ee419e0eSNicolas GERARD
591ee419e0eSNicolas GERARD    /**
592ee419e0eSNicolas GERARD     *
593ee419e0eSNicolas GERARD     * Print the extension/plugin to update
594ee419e0eSNicolas GERARD     *
595ee419e0eSNicolas GERARD     * Note, there is also an Endpoint:
596ee419e0eSNicolas GERARD     * self::EXTENSION_REPOSITORY_API.'?fmt=php&ext[]='.urlencode($name)
597ee419e0eSNicolas GERARD     * `http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php?fmt=php&ext[]=`.urlencode($name)
598ee419e0eSNicolas GERARD     *
599ee419e0eSNicolas GERARD     * @noinspection PhpUndefinedClassInspection
600ee419e0eSNicolas GERARD     */
601ee419e0eSNicolas GERARD    private function pluginToUpdate()
602ee419e0eSNicolas GERARD    {
603ee419e0eSNicolas GERARD
604ee419e0eSNicolas GERARD        if (class_exists(Local::class)) {
605ee419e0eSNicolas GERARD            /**
606ee419e0eSNicolas GERARD             * Release 2025-05-14 "Librarian"
607ee419e0eSNicolas GERARD             * https://www.dokuwiki.org/changes#release_2025-05-14_librarian
608ee419e0eSNicolas GERARD             * https://www.patreon.com/posts/new-extension-116501986
609ee419e0eSNicolas GERARD             * ./bin/plugin.php extension list
610ee419e0eSNicolas GERARD             * @link lib/plugins/extension/cli.php
611ee419e0eSNicolas GERARD             * Code based on https://github.com/giterlizzi/dokuwiki-template-bootstrap3/pull/617/files
612ee419e0eSNicolas GERARD             */
613ee419e0eSNicolas GERARD            try {
614ee419e0eSNicolas GERARD                $extensions = (new Local())->getExtensions();
615ee419e0eSNicolas GERARD                Repository::getInstance()->initExtensions(array_keys($extensions));
616ee419e0eSNicolas GERARD                foreach ($extensions as $extension) {
617ee419e0eSNicolas GERARD                    if ($extension->isEnabled() && $extension->isUpdateAvailable()) {
618ee419e0eSNicolas GERARD                        echo "The extension {$extension->getDisplayName()} should be updated";
619ee419e0eSNicolas GERARD                    }
620ee419e0eSNicolas GERARD                }
621ee419e0eSNicolas GERARD            } /** @noinspection PhpUndefinedClassInspection */ catch (ExtensionException $ignore) {
622ee419e0eSNicolas GERARD                // Ignore the exception
623ee419e0eSNicolas GERARD            }
624ee419e0eSNicolas GERARD            return;
625ee419e0eSNicolas GERARD        }
626ee419e0eSNicolas GERARD
627ee419e0eSNicolas GERARD
628ee419e0eSNicolas GERARD        $pluginList = plugin_list('', true);
629ee419e0eSNicolas GERARD        $extension = $this->loadHelper('extension_extension');
630ee419e0eSNicolas GERARD        foreach ($pluginList as $name) {
631ee419e0eSNicolas GERARD
632ee419e0eSNicolas GERARD            /* @var helper_plugin_extension_extension $extension
633ee419e0eSNicolas GERARD             * old extension manager until Kaos
634ee419e0eSNicolas GERARD             */
635ee419e0eSNicolas GERARD            $extension->setExtension($name);
636ee419e0eSNicolas GERARD            /** @noinspection PhpUndefinedMethodInspection */
637ee419e0eSNicolas GERARD            if ($extension->updateAvailable()) {
638ee419e0eSNicolas GERARD                echo "The extension $name should be updated";
639ee419e0eSNicolas GERARD            }
640ee419e0eSNicolas GERARD        }
641ee419e0eSNicolas GERARD
642ee419e0eSNicolas GERARD
643ee419e0eSNicolas GERARD    }
644ee419e0eSNicolas GERARD
645ee419e0eSNicolas GERARD    /**
646ee419e0eSNicolas GERARD     * @return void
647ee419e0eSNicolas GERARD     * Print the broken Links
648ee419e0eSNicolas GERARD     * @throws ExceptionSqliteNotAvailable
649ee419e0eSNicolas GERARD     */
650ee419e0eSNicolas GERARD    private function brokenLinks()
651ee419e0eSNicolas GERARD    {
652ee419e0eSNicolas GERARD        LogUtility::msg("Broken Links Started");
653ee419e0eSNicolas GERARD        $sqlite = Sqlite::createOrGetSqlite();
654ee419e0eSNicolas GERARD        $request = $sqlite
655ee419e0eSNicolas GERARD            ->createRequest()
656ee419e0eSNicolas GERARD            ->setQuery("with validPages as (select path, analytics
657ee419e0eSNicolas GERARD                     from pages
658ee419e0eSNicolas GERARD                     where json_valid(analytics) = 1)
659ee419e0eSNicolas GERARDselect path,
660ee419e0eSNicolas GERARD       json_extract(analytics, '$.statistics.internal_broken_link_count') as broken_link,
661ee419e0eSNicolas GERARD       json_extract(analytics, '$.statistics.media.internal_broken_count') as broken_media
662ee419e0eSNicolas GERARDfrom validPages
663ee419e0eSNicolas GERARDwhere json_extract(analytics, '$.statistics.internal_broken_link_count') is not null
664ee419e0eSNicolas GERARD   or json_extract(analytics, '$.statistics.media.internal_broken_count') != 0");
665ee419e0eSNicolas GERARD        $rows = [];
666ee419e0eSNicolas GERARD        try {
667ee419e0eSNicolas GERARD            $rows = $request
668ee419e0eSNicolas GERARD                ->execute()
669ee419e0eSNicolas GERARD                ->getRows();
670ee419e0eSNicolas GERARD        } catch (ExceptionCompile $e) {
671ee419e0eSNicolas GERARD            LogUtility::msg("Error while getting the id pages. {$e->getMessage()}");
672ee419e0eSNicolas GERARD            return;
673ee419e0eSNicolas GERARD        } finally {
674ee419e0eSNicolas GERARD            $request->close();
675ee419e0eSNicolas GERARD        }
676*328b625dSNicolas GERARD        if (count($rows) == 0) {
677*328b625dSNicolas GERARD            LogUtility::msg("No Broken Links");
678*328b625dSNicolas GERARD            exit();
679*328b625dSNicolas GERARD        }
680ee419e0eSNicolas GERARD        LogUtility::msg("Broken Links:");
681ee419e0eSNicolas GERARD        foreach ($rows as $row) {
682ee419e0eSNicolas GERARD            $path = $row["path"];
683ee419e0eSNicolas GERARD            $broken_link = $row["broken_link"];
684ee419e0eSNicolas GERARD            $broken_media = $row["broken_media"];
685ee419e0eSNicolas GERARD            echo "$path (Page: $broken_link, Media: $broken_media)    \n";
686ee419e0eSNicolas GERARD        }
687ee419e0eSNicolas GERARD        if (count($rows) != 0) {
688ee419e0eSNicolas GERARD            exit(1);
689ee419e0eSNicolas GERARD        }
690ee419e0eSNicolas GERARD    }
691007225e5Sgerardnico}
692