xref: /plugin/combo/renderer/analytics.php (revision e8b2ff590c848541e718216df3a67061e98c1761)
1<?php
2
3
4use ComboStrap\Analytics;
5use ComboStrap\LinkUtility;
6use ComboStrap\StringUtility;
7
8use ComboStrap\Page;
9use dokuwiki\ChangeLog\PageChangeLog;
10
11require_once(__DIR__ . '/../class/LowQualityPage.php');
12require_once(__DIR__ . '/../class/Analytics.php');
13
14
15/**
16 * A analysis Renderer that exports stats/quality/metadata in a json format
17 * You can export the data with
18 * doku.php?id=somepage&do=export_combo_analytics
19 */
20class renderer_plugin_combo_analytics extends Doku_Renderer
21{
22
23    const DATE_CREATED = 'date_created';
24    const PLAINTEXT = 'formatted';
25    const RESULT = "result";
26    const DESCRIPTION = "description";
27    const PASSED = "Passed";
28    const FAILED = "Failed";
29    const FIXME = 'fixme';
30
31    /**
32     * Rules key
33     */
34    const RULE_WORDS_MINIMAL = 'words_min';
35    const RULE_OUTLINE_STRUCTURE = "outline_structure";
36    const RULE_INTERNAL_BACKLINKS_MIN = 'internal_backlinks_min';
37    const RULE_WORDS_MAXIMAL = "words_max";
38    const RULE_AVERAGE_WORDS_BY_SECTION_MIN = 'words_by_section_avg_min';
39    const RULE_AVERAGE_WORDS_BY_SECTION_MAX = 'words_by_section_avg_max';
40    const RULE_INTERNAL_LINKS_MIN = 'internal_links_min';
41    const RULE_INTERNAL_BROKEN_LINKS_MAX = 'internal_links_broken_max';
42    const RULE_DESCRIPTION_PRESENT = 'description_present';
43    const RULE_FIXME = "fixme_min";
44    const RULE_TITLE_PRESENT = "title_present";
45    const RULE_CANONICAL_PRESENT = "canonical_present";
46    const QUALITY_RULES = [
47        self::RULE_CANONICAL_PRESENT,
48        self::RULE_DESCRIPTION_PRESENT,
49        self::RULE_FIXME,
50        self::RULE_INTERNAL_BACKLINKS_MIN,
51        self::RULE_INTERNAL_BROKEN_LINKS_MAX,
52        self::RULE_INTERNAL_LINKS_MIN,
53        self::RULE_OUTLINE_STRUCTURE,
54        self::RULE_TITLE_PRESENT,
55        self::RULE_WORDS_MINIMAL,
56        self::RULE_WORDS_MAXIMAL,
57        self::RULE_AVERAGE_WORDS_BY_SECTION_MIN,
58        self::RULE_AVERAGE_WORDS_BY_SECTION_MAX
59    ];
60
61    /**
62     * The default man
63     */
64    const CONF_MANDATORY_QUALITY_RULES_DEFAULT_VALUE = [
65        self::RULE_WORDS_MINIMAL,
66        self::RULE_INTERNAL_BACKLINKS_MIN,
67        self::RULE_INTERNAL_LINKS_MIN
68    ];
69    const CONF_MANDATORY_QUALITY_RULES = "mandatoryQualityRules";
70
71    /**
72     * Quality Score factors
73     * They are used to calculate the score
74     */
75    const CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR = 'qualityScoreInternalBacklinksFactor';
76    const CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR = 'qualityScoreInternalLinksFactor';
77    const CONF_QUALITY_SCORE_TITLE_PRESENT = 'qualityScoreTitlePresent';
78    const CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE = 'qualityScoreCorrectOutline';
79    const CONF_QUALITY_SCORE_CORRECT_CONTENT = 'qualityScoreCorrectContentLength';
80    const CONF_QUALITY_SCORE_NO_FIXME = 'qualityScoreNoFixMe';
81    const CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE = 'qualityScoreCorrectWordSectionAvg';
82    const CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR = 'qualityScoreNoBrokenLinks';
83    const CONF_QUALITY_SCORE_CHANGES_FACTOR = 'qualityScoreChangesFactor';
84    const CONF_QUALITY_SCORE_DESCRIPTION_PRESENT = 'qualityScoreDescriptionPresent';
85    const CONF_QUALITY_SCORE_CANONICAL_PRESENT = 'qualityScoreCanonicalPresent';
86    const SCORING = "scoring";
87    const SCORE = "score";
88    const HEADER_STRUCT = 'header_struct';
89    const RENDERER_NAME_MODE = "combo_" . renderer_plugin_combo_analytics::RENDERER_FORMAT;
90    /**
91     * The format returned by the renderer
92     */
93    const RENDERER_FORMAT = "analytics";
94
95
96    /**
97     * The processing data
98     * that should be {@link  renderer_plugin_combo_analysis::reset()}
99     */
100    public $stats = array(); // the stats
101    protected $analyticsMetadata = array(); // the metadata
102    protected $headerId = 0; // the id of the header on the page (first, second, ...)
103
104    /**
105     * Don't known this variable ?
106     */
107    protected $quotelevel = 0;
108    protected $formattingBracket = 0;
109    protected $tableopen = false;
110    private $plainTextId = 0;
111    /**
112     * @var Page
113     */
114    private $page;
115
116    /**
117     * Get and unset a value from an array
118     * @param array $array
119     * @param $key
120     * @param $default
121     * @return mixed
122     */
123    private static function getAndUnset(array &$array, $key, $default)
124    {
125        if (isset($array[$key])) {
126            $value = $array[$key];
127            unset($array[$key]);
128            return $value;
129        }
130        return $default;
131
132    }
133
134    public function document_start()
135    {
136        $this->reset();
137        $this->page = Page::createPageFromCurrentId();
138
139    }
140
141
142    /**
143     * Here the score is calculated
144     */
145    public function document_end() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
146    {
147        /**
148         * The exported object
149         */
150        $statExport = $this->stats;
151
152        /**
153         * The metadata
154         */
155        global $ID;
156        $dokuWikiMetadata = p_get_metadata($ID);
157
158        /**
159         * Edit author stats
160         */
161        $changelog = new PageChangeLog($ID);
162        $revs = $changelog->getRevisions(0, 10000);
163        array_push($revs, $dokuWikiMetadata['last_change']['date']);
164        $statExport[Analytics::EDITS_COUNT] = count($revs);
165        foreach ($revs as $rev) {
166
167
168            /**
169             * Init the authors array
170             */
171            if (!array_key_exists('authors', $statExport)) {
172                $statExport['authors'] = [];
173            }
174            /**
175             * Analytics by users
176             */
177            $info = $changelog->getRevisionInfo($rev);
178            if (is_array($info)) {
179                $user = "*";
180                if (array_key_exists('user', $info)) {
181                    $user = $info['user'];
182                }
183                if (!array_key_exists('authors', $statExport['authors'])) {
184                    $statExport['authors'][$user] = 0;
185                }
186                $statExport['authors'][$user] += 1;
187            }
188        }
189
190        /**
191         * Word and chars count
192         * The word count does not take into account
193         * words with non-words characters such as < =
194         * Therefore the node and attribute are not taken in the count
195         */
196        $text = rawWiki($ID);
197        $statExport[Analytics::CHARS_COUNT] = strlen($text);
198        $statExport[Analytics::WORDS_COUNT] = StringUtility::getWordCount($text);
199
200
201        /**
202         * Internal link distance summary calculation
203         */
204        if (array_key_exists(Analytics::INTERNAL_LINK_DISTANCE, $statExport)) {
205            $linkLengths = $statExport[Analytics::INTERNAL_LINK_DISTANCE];
206            unset($statExport[Analytics::INTERNAL_LINK_DISTANCE]);
207            $countBacklinks = count($linkLengths);
208            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = null;
209            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = null;
210            $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = null;
211            if ($countBacklinks > 0) {
212                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = array_sum($linkLengths) / $countBacklinks;
213                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = max($linkLengths);
214                $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = min($linkLengths);
215            }
216        }
217
218        /**
219         * Quality Report / Rules
220         */
221        // The array that hold the results of the quality rules
222        $ruleResults = array();
223        // The array that hold the quality score details
224        $qualityScores = array();
225
226
227        /**
228         * No fixme
229         */
230        if (array_key_exists(self::FIXME, $this->stats)) {
231            $fixmeCount = $this->stats[self::FIXME];
232            $statExport[self::FIXME] = $fixmeCount == null ? 0 : $fixmeCount;
233            if ($fixmeCount != 0) {
234                $ruleResults[self::RULE_FIXME] = self::FAILED;
235                $qualityScores['no_' . self::FIXME] = 0;
236            } else {
237                $ruleResults[self::RULE_FIXME] = self::PASSED;
238                $qualityScores['no_' . self::FIXME] = $this->getConf(self::CONF_QUALITY_SCORE_NO_FIXME, 1);
239            }
240        }
241
242        /**
243         * A title should be present
244         */
245        $titleScore = $this->getConf(self::CONF_QUALITY_SCORE_TITLE_PRESENT, 10);
246        if (empty($this->analyticsMetadata[Analytics::TITLE])) {
247            $ruleResults[self::RULE_TITLE_PRESENT] = self::FAILED;
248            $ruleInfo[self::RULE_TITLE_PRESENT] = "Add a title in the frontmatter for {$titleScore} points";
249            $this->analyticsMetadata[Analytics::TITLE] = $dokuWikiMetadata[Analytics::TITLE];
250            $qualityScores[self::RULE_TITLE_PRESENT] = 0;
251        } else {
252            $qualityScores[self::RULE_TITLE_PRESENT] = $titleScore;
253            $ruleResults[self::RULE_TITLE_PRESENT] = self::PASSED;
254        }
255
256        /**
257         * A description should be present
258         */
259        $descScore = $this->getConf(self::CONF_QUALITY_SCORE_DESCRIPTION_PRESENT, 8);
260        if (empty($this->analyticsMetadata[self::DESCRIPTION])) {
261            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::FAILED;
262            $ruleInfo[self::RULE_DESCRIPTION_PRESENT] = "Add a description in the frontmatter for {$descScore} points";
263            $this->analyticsMetadata[self::DESCRIPTION] = $dokuWikiMetadata[self::DESCRIPTION]["abstract"];
264            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = 0;
265        } else {
266            $qualityScores[self::RULE_DESCRIPTION_PRESENT] = $descScore;
267            $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::PASSED;
268        }
269
270        /**
271         * A canonical should be present
272         */
273        $canonicalScore = $this->getConf(self::CONF_QUALITY_SCORE_CANONICAL_PRESENT, 5);
274        if (empty($this->analyticsMetadata[Page::CANONICAL_PROPERTY])) {
275            global $conf;
276            $root = $conf['start'];
277            if ($ID != $root) {
278                $qualityScores[self::RULE_CANONICAL_PRESENT] = 0;
279                $ruleResults[self::RULE_CANONICAL_PRESENT] = self::FAILED;
280                $ruleInfo[self::RULE_CANONICAL_PRESENT] = "Add a canonical in the frontmatter for {$canonicalScore} points";
281            }
282        } else {
283            $qualityScores[self::RULE_CANONICAL_PRESENT] = $canonicalScore;
284            $ruleResults[self::RULE_CANONICAL_PRESENT] = self::PASSED;
285        }
286
287        /**
288         * Outline / Header structure
289         */
290        $treeError = 0;
291        $headersCount = 0;
292        if (array_key_exists(Analytics::HEADER_POSITION, $this->stats)) {
293            $headersCount = count($this->stats[Analytics::HEADER_POSITION]);
294            unset($statExport[Analytics::HEADER_POSITION]);
295            for ($i = 1; $i < $headersCount; $i++) {
296                $currentHeaderLevel = $this->stats[self::HEADER_STRUCT][$i];
297                $previousHeaderLevel = $this->stats[self::HEADER_STRUCT][$i - 1];
298                if ($currentHeaderLevel - $previousHeaderLevel > 1) {
299                    $treeError += 1;
300                    $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "The " . $i . " header (h" . $currentHeaderLevel . ") has a level bigger than its precedent (" . $previousHeaderLevel . ")";
301                }
302            }
303            unset($statExport[self::HEADER_STRUCT]);
304        }
305        $outlinePoints = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE, 3);
306        if ($treeError > 0 || $headersCount == 0) {
307            $qualityScores['correct_outline'] = 0;
308            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::FAILED;
309            if ($headersCount == 0) {
310                $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "Add headings to create a document outline for {$outlinePoints} points";
311            }
312        } else {
313            $qualityScores['correct_outline'] = $outlinePoints;
314            $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::PASSED;
315        }
316
317
318        /**
319         * Document length
320         */
321        $minimalWordCount = 50;
322        $maximalWordCount = 1500;
323        $correctContentLength = true;
324        $correctLengthScore = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_CONTENT, 10);
325        $missingWords = $minimalWordCount - $statExport[Analytics::WORDS_COUNT];
326        if ($missingWords > 0) {
327            $ruleResults[self::RULE_WORDS_MINIMAL] = self::FAILED;
328            $correctContentLength = false;
329            $ruleInfo[self::RULE_WORDS_MINIMAL] = "Add {$missingWords} words to get {$correctLengthScore} points";
330        } else {
331            $ruleResults[self::RULE_WORDS_MINIMAL] = self::PASSED;
332        }
333        $tooMuchWords = $statExport[Analytics::WORDS_COUNT] - $maximalWordCount;
334        if ($tooMuchWords > 0) {
335            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::FAILED;
336            $ruleInfo[self::RULE_WORDS_MAXIMAL] = "Delete {$tooMuchWords} words to get {$correctLengthScore} points";
337            $correctContentLength = false;
338        } else {
339            $ruleResults[self::RULE_WORDS_MAXIMAL] = self::PASSED;
340        }
341        if ($correctContentLength) {
342            $qualityScores['correct_content_length'] = $correctLengthScore;
343        } else {
344            $qualityScores['correct_content_length'] = 0;
345        }
346
347
348        /**
349         * Average Number of words by header section to text ratio
350         */
351        $headers = $this->stats[Analytics::HEADERS_COUNT];
352        if ($headers != null) {
353            $headerCount = array_sum($headers);
354            $headerCount--; // h1 is supposed to have no words
355            if ($headerCount > 0) {
356
357                $avgWordsCountBySection = round($this->stats[Analytics::WORDS_COUNT] / $headerCount);
358                $statExport['word_section_count']['avg'] = $avgWordsCountBySection;
359
360                /**
361                 * Min words by header section
362                 */
363                $wordsByHeaderMin = 20;
364                /**
365                 * Max words by header section
366                 */
367                $wordsByHeaderMax = 300;
368                $correctAverageWordsBySection = true;
369                if ($avgWordsCountBySection < $wordsByHeaderMin) {
370                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::FAILED;
371                    $correctAverageWordsBySection = false;
372                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = "The number of words by section is less than {$wordsByHeaderMin}";
373                } else {
374                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::PASSED;
375                }
376                if ($avgWordsCountBySection > $wordsByHeaderMax) {
377                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::FAILED;
378                    $correctAverageWordsBySection = false;
379                    $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is more than {$wordsByHeaderMax}";
380                } else {
381                    $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::PASSED;
382                }
383                if ($correctAverageWordsBySection) {
384                    $qualityScores['correct_word_avg_by_section'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE, 10);
385                } else {
386                    $qualityScores['correct_word_avg_by_section'] = 0;
387                }
388
389            }
390        }
391
392        /**
393         * Internal Backlinks rule
394         *
395         * If a page is a low quality page, if the process run
396         * anonymous, we will not see all {@link ft_backlinks()}
397         * we use then the index directly to avoid confusion
398         */
399        $backlinks = idx_get_indexer()->lookupKey('relation_references', $ID);
400        $countBacklinks = count($backlinks);
401        $statExport[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks;
402        $backlinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1);
403        if ($countBacklinks == 0) {
404            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = 0;
405            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED;
406            $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "Add backlinks for {$backlinkScore} point each";
407        } else {
408
409            $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks * $backlinkScore;
410            $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED;
411        }
412
413        /**
414         * Internal links
415         */
416        $internalLinksCount = $this->stats[Analytics::INTERNAL_LINKS_COUNT];
417        $internalLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1);
418        if ($internalLinksCount == 0) {
419            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = 0;
420            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED;
421            $ruleInfo[self::RULE_INTERNAL_LINKS_MIN] = "Add internal links for {$internalLinkScore} point each";
422        } else {
423            $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED;
424            $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = $countBacklinks * $internalLinkScore;
425        }
426
427        /**
428         * Broken Links
429         */
430        $brokenLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2);
431        $brokenLinksCount = 0;
432        if (array_key_exists(Analytics::INTERNAL_LINKS_BROKEN_COUNT, $this->stats)) {
433            $brokenLinksCount = $this->stats[Analytics::INTERNAL_LINKS_BROKEN_COUNT];
434        }
435        if ($brokenLinksCount > 2) {
436            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = 0;
437            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED;
438            $ruleInfo[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = "Delete the {$brokenLinksCount} broken links and add {$brokenLinkScore} points";
439        } else {
440            $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = $brokenLinkScore;
441            $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED;
442        }
443
444        /**
445         * Media
446         */
447        $mediasStats = [
448            "total_count" => self::getAndUnset($statExport, Analytics::MEDIAS_COUNT, 0),
449            "internal_count" => self::getAndUnset($statExport, Analytics::INTERNAL_MEDIAS_COUNT, 0),
450            "internal_broken_count" => self::getAndUnset($statExport, Analytics::INTERNAL_BROKEN_MEDIAS_COUNT,0),
451            "external_count" => self::getAndUnset($statExport, Analytics::EXTERNAL_MEDIAS_COUNT,0)
452        ];
453        $statExport['media'] = $mediasStats;
454
455        /**
456         * Changes, the more changes the better
457         */
458        $qualityScores[Analytics::EDITS_COUNT] = $statExport[Analytics::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25);
459
460
461        /**
462         * Quality Score
463         */
464        ksort($qualityScores);
465        $qualityScoring = array();
466        $qualityScoring[self::SCORE] = array_sum($qualityScores);
467        $qualityScoring["scores"] = $qualityScores;
468
469
470        /**
471         * The rule that if broken will set the quality level to low
472         */
473        $brokenRules = array();
474        foreach ($ruleResults as $ruleName => $ruleResult) {
475            if ($ruleResult == self::FAILED) {
476                $brokenRules[] = $ruleName;
477            }
478        }
479        $ruleErrorCount = sizeof($brokenRules);
480        if ($ruleErrorCount > 0) {
481            $qualityResult = $ruleErrorCount . " quality rules errors";
482        } else {
483            $qualityResult = "All quality rules passed";
484        }
485
486        /**
487         * Low level Computation
488         */
489        $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES));
490        $mandatoryRulesBroken = [];
491        foreach ($mandatoryRules as $lowLevelRule) {
492            if (in_array($lowLevelRule, $brokenRules)) {
493                $mandatoryRulesBroken[] = $lowLevelRule;
494            }
495        }
496        /**
497         * If the low level is not set manually
498         */
499        if (empty($this->analyticsMetadata[Page::LOW_QUALITY_PAGE_INDICATOR])) {
500            $lowLevel = false;
501            $brokenRulesCount = sizeof($mandatoryRulesBroken);
502            if ($brokenRulesCount > 0) {
503                $lowLevel = true;
504                $quality["message"] = "$brokenRulesCount mandatory rules broken.";
505            } else {
506                $quality["message"] = "No mandatory rules broken";
507            }
508        } else {
509            $lowLevel = filter_var($this->analyticsMetadata[Page::LOW_QUALITY_PAGE_INDICATOR], FILTER_VALIDATE_BOOLEAN);
510        }
511        if (!$this->page->isSlot()) {
512            $this->page->setLowQualityIndicator($lowLevel);
513        } else {
514            $this->page->setLowQualityIndicator(false);
515        }
516
517        /**
518         * Building the quality object in order
519         */
520        $quality[Analytics::LOW] = $lowLevel;
521        if (sizeof($mandatoryRulesBroken) > 0) {
522            ksort($mandatoryRulesBroken);
523            $quality[Analytics::FAILED_MANDATORY_RULES] = $mandatoryRulesBroken;
524        }
525        $quality[self::SCORING] = $qualityScoring;
526        $quality[Analytics::RULES][self::RESULT] = $qualityResult;
527        if (!empty($ruleInfo)) {
528            $quality[Analytics::RULES]["info"] = $ruleInfo;
529        }
530
531        ksort($ruleResults);
532        $quality[Analytics::RULES][Analytics::DETAILS] = $ruleResults;
533
534        /**
535         * Metadata
536         */
537        $title = $dokuWikiMetadata['title'];
538        $this->analyticsMetadata[Analytics::TITLE] = $title;
539        if ($title != @$dokuWikiMetadata[Analytics::H1]) {
540            $this->analyticsMetadata[Analytics::H1] = $dokuWikiMetadata[Analytics::H1];
541        }
542        $timestampCreation = $dokuWikiMetadata['date']['created'];
543        $this->analyticsMetadata[self::DATE_CREATED] = date('Y-m-d h:i:s', $timestampCreation);
544        $timestampModification = $dokuWikiMetadata['date']['modified'];
545        $this->analyticsMetadata[Analytics::DATE_MODIFIED] = date('Y-m-d h:i:s', $timestampModification);
546        $this->analyticsMetadata['age_creation'] = round((time() - $timestampCreation) / 60 / 60 / 24);
547        $this->analyticsMetadata['age_modification'] = round((time() - $timestampModification) / 60 / 60 / 24);
548
549
550        /**
551         * Building the Top JSON in order
552         */
553        global $ID;
554        $finalStats = array();
555        $finalStats["id"] = $ID;
556        $finalStats["date"] = date('Y-m-d H:i:s', time());
557        $finalStats['metadata'] = $this->analyticsMetadata;
558        ksort($statExport);
559        $finalStats[Analytics::STATISTICS] = $statExport;
560        $finalStats[Analytics::QUALITY] = $quality; // Quality after the sort to get them at the end
561
562
563        /**
564         * The result can be seen with
565         * doku.php?id=somepage&do=export_combo_analysis
566         *
567         * Set the header temporarily for the export.php file
568         *
569         * The mode in the export is
570         */
571        $mode = "combo_" . $this->getPluginComponent();
572        p_set_metadata(
573            $ID,
574            array("format" => array($mode => array("Content-Type" => 'application/json'))),
575            false,
576            true // Persistence is needed because there is a cache
577        );
578        $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT);
579
580        $this->page->saveAnalytics($finalStats);
581        $this->doc .= $json_encoded;
582
583    }
584
585    /**
586     */
587    public function getFormat()
588    {
589        return self::RENDERER_FORMAT;
590    }
591
592    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content')
593    {
594
595        $link = new LinkUtility($id);
596        $link->setType(LinkUtility::TYPE_INTERNAL);
597        $link->processLinkStats($this->stats);
598
599    }
600
601    public function externallink($url, $name = null)
602    {
603        $link = new LinkUtility($url);
604        $link->setType(LinkUtility::TYPE_EXTERNAL);
605        if ($name != null) {
606            $link->setName($name);
607        }
608        $link->processLinkStats($this->stats);
609    }
610
611    public function header($text, $level, $pos)
612    {
613        if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) {
614            $this->stats[Analytics::HEADERS_COUNT] = [];
615        }
616        $heading = 'h' . $level;
617        if (!array_key_exists(
618            $heading,
619            $this->stats[Analytics::HEADERS_COUNT])) {
620            $this->stats[Analytics::HEADERS_COUNT][$heading] = 0;
621        }
622        $this->stats[Analytics::HEADERS_COUNT][$heading]++;
623
624        $this->headerId++;
625        $this->stats[Analytics::HEADER_POSITION][$this->headerId] = $heading;
626
627        /**
628         * Store the level of each heading
629         * They should only go from low to highest value
630         * for a good outline
631         */
632        if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) {
633            $this->stats[self::HEADER_STRUCT] = [];
634        }
635        $this->stats[self::HEADER_STRUCT][] = $level;
636
637    }
638
639    public function smiley($smiley)
640    {
641        if ($smiley == 'FIXME') $this->stats[self::FIXME]++;
642    }
643
644    public function linebreak()
645    {
646        if (!$this->tableopen) {
647            $this->stats['linebreak']++;
648        }
649    }
650
651    public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
652    {
653        $this->tableopen = true;
654    }
655
656    public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
657    {
658        $this->tableopen = false;
659    }
660
661    public function hr()
662    {
663        $this->stats['hr']++;
664    }
665
666    public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
667    {
668        $this->stats['quote_count']++;
669        $this->quotelevel++;
670        $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']);
671    }
672
673    public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
674    {
675        $this->quotelevel--;
676    }
677
678    public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
679    {
680        $this->formattingBracket++;
681    }
682
683    public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
684    {
685        $this->formattingBracket--;
686    }
687
688    public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
689    {
690        $this->formattingBracket++;
691    }
692
693    public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
694    {
695        $this->formattingBracket--;
696    }
697
698    public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
699    {
700        $this->formattingBracket++;
701    }
702
703    public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
704    {
705        $this->formattingBracket--;
706    }
707
708    public function cdata($text)
709    {
710
711        /**
712         * It seems that you receive cdata
713         * when emphasis_open / underline_open / strong_open
714         * Stats are not for them
715         */
716        if (!$this->formattingBracket) return;
717
718        $this->plainTextId++;
719
720        /**
721         * Length
722         */
723        $len = strlen($text);
724        $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len;
725
726
727        /**
728         * Multi-formatting
729         */
730        if ($this->formattingBracket > 1) {
731            $numberOfFormats = 1 * ($this->formattingBracket - 1);
732            $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats;
733        }
734
735        /**
736         * Total
737         */
738        $this->stats[self::PLAINTEXT][0] += $len;
739    }
740
741    public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
742    {
743        $this->stats[Analytics::INTERNAL_MEDIAS_COUNT]++;
744    }
745
746    public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null)
747    {
748        $this->stats[Analytics::EXTERNAL_MEDIAS_COUNT]++;
749    }
750
751    public function reset()
752    {
753        $this->stats = array();
754        $this->analyticsMetadata = array();
755        $this->headerId = 0;
756    }
757
758    public function setMeta($key, $value)
759    {
760        $this->analyticsMetadata[$key] = $value;
761    }
762
763
764}
765
766