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