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