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