1007225e5Sgerardnico<?php 2007225e5Sgerardnico 3007225e5Sgerardnico 4007225e5Sgerardnicouse ComboStrap\Analytics; 5007225e5Sgerardnicouse ComboStrap\LinkUtility; 67c33ecc6Sgerardnicouse ComboStrap\StringUtility; 77c33ecc6Sgerardnico 871f916b9Sgerardnicouse ComboStrap\Page; 9007225e5Sgerardnicouse dokuwiki\ChangeLog\PageChangeLog; 10007225e5Sgerardnico 11007225e5Sgerardnicorequire_once(__DIR__ . '/../class/LowQualityPage.php'); 12007225e5Sgerardnicorequire_once(__DIR__ . '/../class/Analytics.php'); 13007225e5Sgerardnico 14007225e5Sgerardnico 15007225e5Sgerardnico/** 16007225e5Sgerardnico * A analysis Renderer that exports stats/quality/metadata in a json format 17007225e5Sgerardnico * You can export the data with 18007225e5Sgerardnico * doku.php?id=somepage&do=export_combo_analytics 19007225e5Sgerardnico */ 20007225e5Sgerardnicoclass renderer_plugin_combo_analytics extends Doku_Renderer 21007225e5Sgerardnico{ 227c33ecc6Sgerardnico 23007225e5Sgerardnico const DATE_CREATED = 'date_created'; 24007225e5Sgerardnico const PLAINTEXT = 'formatted'; 25007225e5Sgerardnico const RESULT = "result"; 26007225e5Sgerardnico const DESCRIPTION = "description"; 27007225e5Sgerardnico const PASSED = "Passed"; 28007225e5Sgerardnico const FAILED = "Failed"; 29007225e5Sgerardnico const FIXME = 'fixme'; 30007225e5Sgerardnico 31007225e5Sgerardnico /** 32007225e5Sgerardnico * Rules key 33007225e5Sgerardnico */ 34007225e5Sgerardnico const RULE_WORDS_MINIMAL = 'words_min'; 35007225e5Sgerardnico const RULE_OUTLINE_STRUCTURE = "outline_structure"; 36007225e5Sgerardnico const RULE_INTERNAL_BACKLINKS_MIN = 'internal_backlinks_min'; 37007225e5Sgerardnico const RULE_WORDS_MAXIMAL = "words_max"; 38007225e5Sgerardnico const RULE_AVERAGE_WORDS_BY_SECTION_MIN = 'words_by_section_avg_min'; 39007225e5Sgerardnico const RULE_AVERAGE_WORDS_BY_SECTION_MAX = 'words_by_section_avg_max'; 40007225e5Sgerardnico const RULE_INTERNAL_LINKS_MIN = 'internal_links_min'; 41007225e5Sgerardnico const RULE_INTERNAL_BROKEN_LINKS_MAX = 'internal_links_broken_max'; 42007225e5Sgerardnico const RULE_DESCRIPTION_PRESENT = 'description_present'; 43007225e5Sgerardnico const RULE_FIXME = "fixme_min"; 44007225e5Sgerardnico const RULE_TITLE_PRESENT = "title_present"; 45007225e5Sgerardnico const RULE_CANONICAL_PRESENT = "canonical_present"; 46aa3cb38fSgerardnico const QUALITY_RULES = [ 47aa3cb38fSgerardnico self::RULE_CANONICAL_PRESENT, 48aa3cb38fSgerardnico self::RULE_DESCRIPTION_PRESENT, 49aa3cb38fSgerardnico self::RULE_FIXME, 50aa3cb38fSgerardnico self::RULE_INTERNAL_BACKLINKS_MIN, 51aa3cb38fSgerardnico self::RULE_INTERNAL_BROKEN_LINKS_MAX, 52aa3cb38fSgerardnico self::RULE_INTERNAL_LINKS_MIN, 53aa3cb38fSgerardnico self::RULE_OUTLINE_STRUCTURE, 54aa3cb38fSgerardnico self::RULE_TITLE_PRESENT, 55aa3cb38fSgerardnico self::RULE_WORDS_MINIMAL, 56aa3cb38fSgerardnico self::RULE_WORDS_MAXIMAL, 57aa3cb38fSgerardnico self::RULE_AVERAGE_WORDS_BY_SECTION_MIN, 58aa3cb38fSgerardnico self::RULE_AVERAGE_WORDS_BY_SECTION_MAX 59aa3cb38fSgerardnico ]; 60007225e5Sgerardnico 61007225e5Sgerardnico /** 62007225e5Sgerardnico * The default man 63007225e5Sgerardnico */ 64007225e5Sgerardnico const CONF_MANDATORY_QUALITY_RULES_DEFAULT_VALUE = [ 65007225e5Sgerardnico self::RULE_WORDS_MINIMAL, 66007225e5Sgerardnico self::RULE_INTERNAL_BACKLINKS_MIN, 67007225e5Sgerardnico self::RULE_INTERNAL_LINKS_MIN 68007225e5Sgerardnico ]; 69007225e5Sgerardnico const CONF_MANDATORY_QUALITY_RULES = "mandatoryQualityRules"; 70007225e5Sgerardnico 71007225e5Sgerardnico /** 72007225e5Sgerardnico * Quality Score factors 73007225e5Sgerardnico * They are used to calculate the score 74007225e5Sgerardnico */ 75007225e5Sgerardnico const CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR = 'qualityScoreInternalBacklinksFactor'; 76007225e5Sgerardnico const CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR = 'qualityScoreInternalLinksFactor'; 77007225e5Sgerardnico const CONF_QUALITY_SCORE_TITLE_PRESENT = 'qualityScoreTitlePresent'; 78007225e5Sgerardnico const CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE = 'qualityScoreCorrectOutline'; 79007225e5Sgerardnico const CONF_QUALITY_SCORE_CORRECT_CONTENT = 'qualityScoreCorrectContentLength'; 80007225e5Sgerardnico const CONF_QUALITY_SCORE_NO_FIXME = 'qualityScoreNoFixMe'; 81007225e5Sgerardnico const CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE = 'qualityScoreCorrectWordSectionAvg'; 82007225e5Sgerardnico const CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR = 'qualityScoreNoBrokenLinks'; 83007225e5Sgerardnico const CONF_QUALITY_SCORE_CHANGES_FACTOR = 'qualityScoreChangesFactor'; 84007225e5Sgerardnico const CONF_QUALITY_SCORE_DESCRIPTION_PRESENT = 'qualityScoreDescriptionPresent'; 85007225e5Sgerardnico const CONF_QUALITY_SCORE_CANONICAL_PRESENT = 'qualityScoreCanonicalPresent'; 8608ca4f85Sgerardnico const SCORING = "scoring"; 8708ca4f85Sgerardnico const SCORE = "score"; 88ebdc69ceSgerardnico const HEADER_STRUCT = 'header_struct'; 89531e725cSNickeau const RENDERER_NAME_MODE = "combo_" . renderer_plugin_combo_analytics::RENDERER_FORMAT; 90531e725cSNickeau /** 91531e725cSNickeau * The format returned by the renderer 92531e725cSNickeau */ 93531e725cSNickeau const RENDERER_FORMAT = "analytics"; 94007225e5Sgerardnico 95aa3cb38fSgerardnico 96007225e5Sgerardnico /** 97007225e5Sgerardnico * The processing data 98007225e5Sgerardnico * that should be {@link renderer_plugin_combo_analysis::reset()} 99007225e5Sgerardnico */ 100007225e5Sgerardnico public $stats = array(); // the stats 101fa5961eaSgerardnico protected $analyticsMetadata = array(); // the metadata 102007225e5Sgerardnico protected $headerId = 0; // the id of the header on the page (first, second, ...) 103007225e5Sgerardnico 104007225e5Sgerardnico /** 105007225e5Sgerardnico * Don't known this variable ? 106007225e5Sgerardnico */ 107007225e5Sgerardnico protected $quotelevel = 0; 108007225e5Sgerardnico protected $formattingBracket = 0; 109007225e5Sgerardnico protected $tableopen = false; 110007225e5Sgerardnico private $plainTextId = 0; 1112c067407Sgerardnico /** 1122c067407Sgerardnico * @var Page 1132c067407Sgerardnico */ 1142c067407Sgerardnico private $page; 1152c067407Sgerardnico 1162c067407Sgerardnico public function document_start() 1172c067407Sgerardnico { 1187c33ecc6Sgerardnico $this->reset(); 119*85e82846SNickeau $this->page = Page::createPageFromCurrentId(); 1202c067407Sgerardnico 1212c067407Sgerardnico } 122007225e5Sgerardnico 123007225e5Sgerardnico 124007225e5Sgerardnico /** 125007225e5Sgerardnico * Here the score is calculated 126007225e5Sgerardnico */ 127007225e5Sgerardnico public function document_end() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 128007225e5Sgerardnico { 129007225e5Sgerardnico /** 130f3748b38Sgerardnico * The exported object 131f3748b38Sgerardnico */ 132f3748b38Sgerardnico $statExport = $this->stats; 133f3748b38Sgerardnico 134f3748b38Sgerardnico /** 135007225e5Sgerardnico * The metadata 136007225e5Sgerardnico */ 137007225e5Sgerardnico global $ID; 138fa5961eaSgerardnico $dokuWikiMetadata = p_get_metadata($ID); 139007225e5Sgerardnico 140007225e5Sgerardnico /** 141f3748b38Sgerardnico * Edit author stats 142f3748b38Sgerardnico */ 143f3748b38Sgerardnico $changelog = new PageChangeLog($ID); 144f3748b38Sgerardnico $revs = $changelog->getRevisions(0, 10000); 145fa5961eaSgerardnico array_push($revs, $dokuWikiMetadata['last_change']['date']); 146f3748b38Sgerardnico $statExport[Analytics::EDITS_COUNT] = count($revs); 147f3748b38Sgerardnico foreach ($revs as $rev) { 1482128d419Sgerardnico 149ebdc69ceSgerardnico 150ebdc69ceSgerardnico /** 151ebdc69ceSgerardnico * Init the authors array 152ebdc69ceSgerardnico */ 153ebdc69ceSgerardnico if (!array_key_exists('authors', $statExport)) { 154ebdc69ceSgerardnico $statExport['authors'] = []; 155f3748b38Sgerardnico } 156ebdc69ceSgerardnico /** 157ebdc69ceSgerardnico * Analytics by users 158ebdc69ceSgerardnico */ 1592128d419Sgerardnico $info = $changelog->getRevisionInfo($rev); 1602128d419Sgerardnico if (is_array($info)) { 161ebdc69ceSgerardnico $user = "*"; 162ebdc69ceSgerardnico if (array_key_exists('user', $info)) { 163ebdc69ceSgerardnico $user = $info['user']; 164ebdc69ceSgerardnico } 165ebdc69ceSgerardnico if (!array_key_exists('authors', $statExport['authors'])) { 166ebdc69ceSgerardnico $statExport['authors'][$user] = 0; 167ebdc69ceSgerardnico } 168ebdc69ceSgerardnico $statExport['authors'][$user] += 1; 169f3748b38Sgerardnico } 1702128d419Sgerardnico } 171f3748b38Sgerardnico 172f3748b38Sgerardnico /** 173007225e5Sgerardnico * Word and chars count 174007225e5Sgerardnico * The word count does not take into account 175007225e5Sgerardnico * words with non-words characters such as < = 176007225e5Sgerardnico * Therefore the node and attribute are not taken in the count 177007225e5Sgerardnico */ 178007225e5Sgerardnico $text = rawWiki($ID); 179f3748b38Sgerardnico $statExport[Analytics::CHARS_COUNT] = strlen($text); 1807c33ecc6Sgerardnico $statExport[Analytics::WORDS_COUNT] = StringUtility::getWordCount($text); 181007225e5Sgerardnico 182007225e5Sgerardnico 183007225e5Sgerardnico /** 184007225e5Sgerardnico * Internal link distance summary calculation 185007225e5Sgerardnico */ 186007225e5Sgerardnico if (array_key_exists(Analytics::INTERNAL_LINK_DISTANCE, $statExport)) { 187007225e5Sgerardnico $linkLengths = $statExport[Analytics::INTERNAL_LINK_DISTANCE]; 188007225e5Sgerardnico unset($statExport[Analytics::INTERNAL_LINK_DISTANCE]); 189007225e5Sgerardnico $countBacklinks = count($linkLengths); 190007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = null; 191007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = null; 192007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = null; 193007225e5Sgerardnico if ($countBacklinks > 0) { 194007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['avg'] = array_sum($linkLengths) / $countBacklinks; 195007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['max'] = max($linkLengths); 196007225e5Sgerardnico $statExport[Analytics::INTERNAL_LINK_DISTANCE]['min'] = min($linkLengths); 197007225e5Sgerardnico } 198007225e5Sgerardnico } 199007225e5Sgerardnico 200007225e5Sgerardnico /** 201007225e5Sgerardnico * Quality Report / Rules 202007225e5Sgerardnico */ 203007225e5Sgerardnico // The array that hold the results of the quality rules 204007225e5Sgerardnico $ruleResults = array(); 205007225e5Sgerardnico // The array that hold the quality score details 206007225e5Sgerardnico $qualityScores = array(); 207007225e5Sgerardnico 208007225e5Sgerardnico 209007225e5Sgerardnico /** 210007225e5Sgerardnico * No fixme 211007225e5Sgerardnico */ 212ebdc69ceSgerardnico if (array_key_exists(self::FIXME, $this->stats)) { 213007225e5Sgerardnico $fixmeCount = $this->stats[self::FIXME]; 214007225e5Sgerardnico $statExport[self::FIXME] = $fixmeCount == null ? 0 : $fixmeCount; 215007225e5Sgerardnico if ($fixmeCount != 0) { 216007225e5Sgerardnico $ruleResults[self::RULE_FIXME] = self::FAILED; 217007225e5Sgerardnico $qualityScores['no_' . self::FIXME] = 0; 218007225e5Sgerardnico } else { 219007225e5Sgerardnico $ruleResults[self::RULE_FIXME] = self::PASSED; 2207c33ecc6Sgerardnico $qualityScores['no_' . self::FIXME] = $this->getConf(self::CONF_QUALITY_SCORE_NO_FIXME, 1); 221007225e5Sgerardnico } 222ebdc69ceSgerardnico } 223007225e5Sgerardnico 224007225e5Sgerardnico /** 225007225e5Sgerardnico * A title should be present 226007225e5Sgerardnico */ 22708ca4f85Sgerardnico $titleScore = $this->getConf(self::CONF_QUALITY_SCORE_TITLE_PRESENT, 10); 228fa5961eaSgerardnico if (empty($this->analyticsMetadata[Analytics::TITLE])) { 229007225e5Sgerardnico $ruleResults[self::RULE_TITLE_PRESENT] = self::FAILED; 23008ca4f85Sgerardnico $ruleInfo[self::RULE_TITLE_PRESENT] = "Add a title in the frontmatter for {$titleScore} points"; 231fa5961eaSgerardnico $this->analyticsMetadata[Analytics::TITLE] = $dokuWikiMetadata[Analytics::TITLE]; 232007225e5Sgerardnico $qualityScores[self::RULE_TITLE_PRESENT] = 0; 233007225e5Sgerardnico } else { 2347c33ecc6Sgerardnico $qualityScores[self::RULE_TITLE_PRESENT] = $titleScore; 235007225e5Sgerardnico $ruleResults[self::RULE_TITLE_PRESENT] = self::PASSED; 236007225e5Sgerardnico } 237007225e5Sgerardnico 238007225e5Sgerardnico /** 239007225e5Sgerardnico * A description should be present 240007225e5Sgerardnico */ 24108ca4f85Sgerardnico $descScore = $this->getConf(self::CONF_QUALITY_SCORE_DESCRIPTION_PRESENT, 8); 242fa5961eaSgerardnico if (empty($this->analyticsMetadata[self::DESCRIPTION])) { 243007225e5Sgerardnico $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::FAILED; 24408ca4f85Sgerardnico $ruleInfo[self::RULE_DESCRIPTION_PRESENT] = "Add a description in the frontmatter for {$descScore} points"; 245fa5961eaSgerardnico $this->analyticsMetadata[self::DESCRIPTION] = $dokuWikiMetadata[self::DESCRIPTION]["abstract"]; 246007225e5Sgerardnico $qualityScores[self::RULE_DESCRIPTION_PRESENT] = 0; 247007225e5Sgerardnico } else { 2487c33ecc6Sgerardnico $qualityScores[self::RULE_DESCRIPTION_PRESENT] = $descScore; 249007225e5Sgerardnico $ruleResults[self::RULE_DESCRIPTION_PRESENT] = self::PASSED; 250007225e5Sgerardnico } 251007225e5Sgerardnico 252007225e5Sgerardnico /** 253007225e5Sgerardnico * A canonical should be present 254007225e5Sgerardnico */ 25508ca4f85Sgerardnico $canonicalScore = $this->getConf(self::CONF_QUALITY_SCORE_CANONICAL_PRESENT, 5); 256fa5961eaSgerardnico if (empty($this->analyticsMetadata[Page::CANONICAL_PROPERTY])) { 257f3748b38Sgerardnico global $conf; 258f3748b38Sgerardnico $root = $conf['start']; 259f3748b38Sgerardnico if ($ID != $root) { 260007225e5Sgerardnico $qualityScores[self::RULE_CANONICAL_PRESENT] = 0; 261007225e5Sgerardnico $ruleResults[self::RULE_CANONICAL_PRESENT] = self::FAILED; 26208ca4f85Sgerardnico $ruleInfo[self::RULE_CANONICAL_PRESENT] = "Add a canonical in the frontmatter for {$canonicalScore} points"; 263f3748b38Sgerardnico } 264007225e5Sgerardnico } else { 2657c33ecc6Sgerardnico $qualityScores[self::RULE_CANONICAL_PRESENT] = $canonicalScore; 266007225e5Sgerardnico $ruleResults[self::RULE_CANONICAL_PRESENT] = self::PASSED; 267007225e5Sgerardnico } 268007225e5Sgerardnico 269007225e5Sgerardnico /** 270007225e5Sgerardnico * Outline / Header structure 271007225e5Sgerardnico */ 272007225e5Sgerardnico $treeError = 0; 273007225e5Sgerardnico $headersCount = 0; 274007225e5Sgerardnico if (array_key_exists(Analytics::HEADER_POSITION, $this->stats)) { 275007225e5Sgerardnico $headersCount = count($this->stats[Analytics::HEADER_POSITION]); 276007225e5Sgerardnico unset($statExport[Analytics::HEADER_POSITION]); 277007225e5Sgerardnico for ($i = 1; $i < $headersCount; $i++) { 278ebdc69ceSgerardnico $currentHeaderLevel = $this->stats[self::HEADER_STRUCT][$i]; 279ebdc69ceSgerardnico $previousHeaderLevel = $this->stats[self::HEADER_STRUCT][$i - 1]; 280007225e5Sgerardnico if ($currentHeaderLevel - $previousHeaderLevel > 1) { 281007225e5Sgerardnico $treeError += 1; 282007225e5Sgerardnico $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "The " . $i . " header (h" . $currentHeaderLevel . ") has a level bigger than its precedent (" . $previousHeaderLevel . ")"; 283007225e5Sgerardnico } 284007225e5Sgerardnico } 285ebdc69ceSgerardnico unset($statExport[self::HEADER_STRUCT]); 286007225e5Sgerardnico } 287eee76a3dSgerardnico $outlinePoints = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_HEADER_STRUCTURE, 3); 288007225e5Sgerardnico if ($treeError > 0 || $headersCount == 0) { 289007225e5Sgerardnico $qualityScores['correct_outline'] = 0; 290007225e5Sgerardnico $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::FAILED; 291007225e5Sgerardnico if ($headersCount == 0) { 292eee76a3dSgerardnico $ruleInfo[self::RULE_OUTLINE_STRUCTURE] = "Add headings to create a document outline for {$outlinePoints} points"; 293007225e5Sgerardnico } 294007225e5Sgerardnico } else { 295eee76a3dSgerardnico $qualityScores['correct_outline'] = $outlinePoints; 296007225e5Sgerardnico $ruleResults[self::RULE_OUTLINE_STRUCTURE] = self::PASSED; 297007225e5Sgerardnico } 298007225e5Sgerardnico 299007225e5Sgerardnico 300007225e5Sgerardnico /** 301007225e5Sgerardnico * Document length 302007225e5Sgerardnico */ 303007225e5Sgerardnico $minimalWordCount = 50; 304007225e5Sgerardnico $maximalWordCount = 1500; 305007225e5Sgerardnico $correctContentLength = true; 30608ca4f85Sgerardnico $correctLengthScore = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_CONTENT, 10); 30708ca4f85Sgerardnico $missingWords = $minimalWordCount - $statExport[Analytics::WORDS_COUNT]; 30808ca4f85Sgerardnico if ($missingWords > 0) { 309007225e5Sgerardnico $ruleResults[self::RULE_WORDS_MINIMAL] = self::FAILED; 310007225e5Sgerardnico $correctContentLength = false; 31108ca4f85Sgerardnico $ruleInfo[self::RULE_WORDS_MINIMAL] = "Add {$missingWords} words to get {$correctLengthScore} points"; 312007225e5Sgerardnico } else { 313007225e5Sgerardnico $ruleResults[self::RULE_WORDS_MINIMAL] = self::PASSED; 314007225e5Sgerardnico } 31508ca4f85Sgerardnico $tooMuchWords = $statExport[Analytics::WORDS_COUNT] - $maximalWordCount; 31608ca4f85Sgerardnico if ($tooMuchWords > 0) { 317007225e5Sgerardnico $ruleResults[self::RULE_WORDS_MAXIMAL] = self::FAILED; 31808ca4f85Sgerardnico $ruleInfo[self::RULE_WORDS_MAXIMAL] = "Delete {$tooMuchWords} words to get {$correctLengthScore} points"; 319007225e5Sgerardnico $correctContentLength = false; 320007225e5Sgerardnico } else { 321007225e5Sgerardnico $ruleResults[self::RULE_WORDS_MAXIMAL] = self::PASSED; 322007225e5Sgerardnico } 323007225e5Sgerardnico if ($correctContentLength) { 32408ca4f85Sgerardnico $qualityScores['correct_content_length'] = $correctLengthScore; 325007225e5Sgerardnico } else { 326007225e5Sgerardnico $qualityScores['correct_content_length'] = 0; 327007225e5Sgerardnico } 328007225e5Sgerardnico 329007225e5Sgerardnico 330007225e5Sgerardnico /** 331007225e5Sgerardnico * Average Number of words by header section to text ratio 332007225e5Sgerardnico */ 333007225e5Sgerardnico $headers = $this->stats[Analytics::HEADERS_COUNT]; 334007225e5Sgerardnico if ($headers != null) { 335007225e5Sgerardnico $headerCount = array_sum($headers); 336007225e5Sgerardnico $headerCount--; // h1 is supposed to have no words 337007225e5Sgerardnico if ($headerCount > 0) { 338007225e5Sgerardnico 339007225e5Sgerardnico $avgWordsCountBySection = round($this->stats[Analytics::WORDS_COUNT] / $headerCount); 340007225e5Sgerardnico $statExport['word_section_count']['avg'] = $avgWordsCountBySection; 341007225e5Sgerardnico 342007225e5Sgerardnico /** 343007225e5Sgerardnico * Min words by header section 344007225e5Sgerardnico */ 345007225e5Sgerardnico $wordsByHeaderMin = 20; 346007225e5Sgerardnico /** 347007225e5Sgerardnico * Max words by header section 348007225e5Sgerardnico */ 349007225e5Sgerardnico $wordsByHeaderMax = 300; 350007225e5Sgerardnico $correctAverageWordsBySection = true; 351007225e5Sgerardnico if ($avgWordsCountBySection < $wordsByHeaderMin) { 352007225e5Sgerardnico $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::FAILED; 353007225e5Sgerardnico $correctAverageWordsBySection = false; 35408ca4f85Sgerardnico $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = "The number of words by section is less than {$wordsByHeaderMin}"; 355007225e5Sgerardnico } else { 356007225e5Sgerardnico $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MIN] = self::PASSED; 357007225e5Sgerardnico } 358007225e5Sgerardnico if ($avgWordsCountBySection > $wordsByHeaderMax) { 359007225e5Sgerardnico $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::FAILED; 360007225e5Sgerardnico $correctAverageWordsBySection = false; 361007225e5Sgerardnico $ruleInfo[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = "The number of words by section is more than {$wordsByHeaderMax}"; 362007225e5Sgerardnico } else { 363007225e5Sgerardnico $ruleResults[self::RULE_AVERAGE_WORDS_BY_SECTION_MAX] = self::PASSED; 364007225e5Sgerardnico } 365007225e5Sgerardnico if ($correctAverageWordsBySection) { 366007225e5Sgerardnico $qualityScores['correct_word_avg_by_section'] = $this->getConf(self::CONF_QUALITY_SCORE_CORRECT_WORD_SECTION_AVERAGE, 10); 367007225e5Sgerardnico } else { 368007225e5Sgerardnico $qualityScores['correct_word_avg_by_section'] = 0; 369007225e5Sgerardnico } 370007225e5Sgerardnico 371007225e5Sgerardnico } 372007225e5Sgerardnico } 373007225e5Sgerardnico 374007225e5Sgerardnico /** 375007225e5Sgerardnico * Internal Backlinks rule 376007225e5Sgerardnico * 377007225e5Sgerardnico * If a page is a low quality page, if the process run 378007225e5Sgerardnico * anonymous, we will not see all {@link ft_backlinks()} 379007225e5Sgerardnico * we use then the index directly to avoid confusion 380007225e5Sgerardnico */ 381007225e5Sgerardnico $backlinks = idx_get_indexer()->lookupKey('relation_references', $ID); 382007225e5Sgerardnico $countBacklinks = count($backlinks); 383007225e5Sgerardnico $statExport[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks; 384d262537cSgerardnico $backlinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_BACKLINK_FACTOR, 1); 385007225e5Sgerardnico if ($countBacklinks == 0) { 386007225e5Sgerardnico $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = 0; 387007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::FAILED; 388d262537cSgerardnico $ruleInfo[self::RULE_INTERNAL_BACKLINKS_MIN] = "Add backlinks for {$backlinkScore} point each"; 389007225e5Sgerardnico } else { 390d262537cSgerardnico 391d262537cSgerardnico $qualityScores[Analytics::INTERNAL_BACKLINKS_COUNT] = $countBacklinks * $backlinkScore; 392007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_BACKLINKS_MIN] = self::PASSED; 393007225e5Sgerardnico } 394007225e5Sgerardnico 395007225e5Sgerardnico /** 396007225e5Sgerardnico * Internal links 397007225e5Sgerardnico */ 398007225e5Sgerardnico $internalLinksCount = $this->stats[Analytics::INTERNAL_LINKS_COUNT]; 399d262537cSgerardnico $internalLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_FACTOR, 1); 400007225e5Sgerardnico if ($internalLinksCount == 0) { 401007225e5Sgerardnico $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = 0; 402007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::FAILED; 403d262537cSgerardnico $ruleInfo[self::RULE_INTERNAL_LINKS_MIN] = "Add internal links for {$internalLinkScore} point each"; 404007225e5Sgerardnico } else { 405007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_LINKS_MIN] = self::PASSED; 406d262537cSgerardnico $qualityScores[Analytics::INTERNAL_LINKS_COUNT] = $countBacklinks * $internalLinkScore; 407007225e5Sgerardnico } 408007225e5Sgerardnico 409007225e5Sgerardnico /** 410007225e5Sgerardnico * Broken Links 411007225e5Sgerardnico */ 412d262537cSgerardnico $brokenLinkScore = $this->getConf(self::CONF_QUALITY_SCORE_INTERNAL_LINK_BROKEN_FACTOR, 2); 413ebdc69ceSgerardnico $brokenLinksCount = 0; 414ebdc69ceSgerardnico if (array_key_exists(Analytics::INTERNAL_LINKS_BROKEN_COUNT, $this->stats)) { 415007225e5Sgerardnico $brokenLinksCount = $this->stats[Analytics::INTERNAL_LINKS_BROKEN_COUNT]; 416ebdc69ceSgerardnico } 417007225e5Sgerardnico if ($brokenLinksCount > 2) { 418007225e5Sgerardnico $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = 0; 419007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::FAILED; 420d262537cSgerardnico $ruleInfo[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = "Delete the {$brokenLinksCount} broken links and add {$brokenLinkScore} points"; 421007225e5Sgerardnico } else { 422d262537cSgerardnico $qualityScores['no_' . Analytics::INTERNAL_LINKS_BROKEN_COUNT] = $brokenLinkScore; 423007225e5Sgerardnico $ruleResults[self::RULE_INTERNAL_BROKEN_LINKS_MAX] = self::PASSED; 424007225e5Sgerardnico } 425007225e5Sgerardnico 426007225e5Sgerardnico /** 427007225e5Sgerardnico * Changes, the more changes the better 428007225e5Sgerardnico */ 429ebdc69ceSgerardnico $qualityScores[Analytics::EDITS_COUNT] = $statExport[Analytics::EDITS_COUNT] * $this->getConf(self::CONF_QUALITY_SCORE_CHANGES_FACTOR, 0.25); 430007225e5Sgerardnico 431007225e5Sgerardnico 432007225e5Sgerardnico /** 433007225e5Sgerardnico * Quality Score 434007225e5Sgerardnico */ 435007225e5Sgerardnico ksort($qualityScores); 436007225e5Sgerardnico $qualityScoring = array(); 43708ca4f85Sgerardnico $qualityScoring[self::SCORE] = array_sum($qualityScores); 438007225e5Sgerardnico $qualityScoring["scores"] = $qualityScores; 439007225e5Sgerardnico 440007225e5Sgerardnico 441007225e5Sgerardnico /** 442007225e5Sgerardnico * The rule that if broken will set the quality level to low 443007225e5Sgerardnico */ 444007225e5Sgerardnico $brokenRules = array(); 445007225e5Sgerardnico foreach ($ruleResults as $ruleName => $ruleResult) { 446007225e5Sgerardnico if ($ruleResult == self::FAILED) { 447007225e5Sgerardnico $brokenRules[] = $ruleName; 448007225e5Sgerardnico } 449007225e5Sgerardnico } 450007225e5Sgerardnico $ruleErrorCount = sizeof($brokenRules); 451007225e5Sgerardnico if ($ruleErrorCount > 0) { 452007225e5Sgerardnico $qualityResult = $ruleErrorCount . " quality rules errors"; 453007225e5Sgerardnico } else { 454007225e5Sgerardnico $qualityResult = "All quality rules passed"; 455007225e5Sgerardnico } 456007225e5Sgerardnico 457007225e5Sgerardnico /** 458fa5961eaSgerardnico * Low level Computation 459007225e5Sgerardnico */ 460007225e5Sgerardnico $mandatoryRules = preg_split("/,/", $this->getConf(self::CONF_MANDATORY_QUALITY_RULES)); 461007225e5Sgerardnico $mandatoryRulesBroken = []; 462007225e5Sgerardnico foreach ($mandatoryRules as $lowLevelRule) { 463007225e5Sgerardnico if (in_array($lowLevelRule, $brokenRules)) { 464007225e5Sgerardnico $mandatoryRulesBroken[] = $lowLevelRule; 465007225e5Sgerardnico } 466007225e5Sgerardnico } 467fa5961eaSgerardnico /** 4686f847fc2Sgerardnico * If the low level is not set manually 469fa5961eaSgerardnico */ 470fa5961eaSgerardnico if (empty($this->analyticsMetadata[Page::LOW_QUALITY_PAGE_INDICATOR])) { 471007225e5Sgerardnico $lowLevel = false; 472*85e82846SNickeau $brokenRulesCount = sizeof($mandatoryRulesBroken); 473*85e82846SNickeau if ($brokenRulesCount > 0) { 474007225e5Sgerardnico $lowLevel = true; 475*85e82846SNickeau $quality["message"] = "$brokenRulesCount mandatory rules broken."; 476*85e82846SNickeau } else { 477*85e82846SNickeau $quality["message"] = "No mandatory rules broken"; 478007225e5Sgerardnico } 4799b9e6d1fSgerardnico } else { 4806f847fc2Sgerardnico $lowLevel = filter_var($this->analyticsMetadata[Page::LOW_QUALITY_PAGE_INDICATOR], FILTER_VALIDATE_BOOLEAN); 4819b9e6d1fSgerardnico } 482531e725cSNickeau if (!$this->page->isSlot()) { 4836f847fc2Sgerardnico $this->page->setLowQualityIndicator($lowLevel); 4845f891b7eSNickeau } else { 4855f891b7eSNickeau $this->page->setLowQualityIndicator(false); 4865f891b7eSNickeau } 487007225e5Sgerardnico 488007225e5Sgerardnico /** 489007225e5Sgerardnico * Building the quality object in order 490007225e5Sgerardnico */ 491f3748b38Sgerardnico $quality[Analytics::LOW] = $lowLevel; 492007225e5Sgerardnico if (sizeof($mandatoryRulesBroken) > 0) { 493007225e5Sgerardnico ksort($mandatoryRulesBroken); 494722648eaSgerardnico $quality[Analytics::FAILED_MANDATORY_RULES] = $mandatoryRulesBroken; 495007225e5Sgerardnico } 49608ca4f85Sgerardnico $quality[self::SCORING] = $qualityScoring; 497f3748b38Sgerardnico $quality[Analytics::RULES][self::RESULT] = $qualityResult; 498007225e5Sgerardnico if (!empty($ruleInfo)) { 499f3748b38Sgerardnico $quality[Analytics::RULES]["info"] = $ruleInfo; 500007225e5Sgerardnico } 501007225e5Sgerardnico 502007225e5Sgerardnico ksort($ruleResults); 503f3748b38Sgerardnico $quality[Analytics::RULES][Analytics::DETAILS] = $ruleResults; 504007225e5Sgerardnico 505007225e5Sgerardnico /** 506007225e5Sgerardnico * Metadata 507007225e5Sgerardnico */ 508fa5961eaSgerardnico $title = $dokuWikiMetadata['title']; 509fa5961eaSgerardnico $this->analyticsMetadata[Analytics::TITLE] = $title; 510531e725cSNickeau if ($title != @$dokuWikiMetadata[Analytics::H1]) { 511531e725cSNickeau $this->analyticsMetadata[Analytics::H1] = $dokuWikiMetadata[Analytics::H1]; 512c42a1196Sgerardnico } 513fa5961eaSgerardnico $timestampCreation = $dokuWikiMetadata['date']['created']; 514fa5961eaSgerardnico $this->analyticsMetadata[self::DATE_CREATED] = date('Y-m-d h:i:s', $timestampCreation); 515fa5961eaSgerardnico $timestampModification = $dokuWikiMetadata['date']['modified']; 516fa5961eaSgerardnico $this->analyticsMetadata[Analytics::DATE_MODIFIED] = date('Y-m-d h:i:s', $timestampModification); 517fa5961eaSgerardnico $this->analyticsMetadata['age_creation'] = round((time() - $timestampCreation) / 60 / 60 / 24); 518fa5961eaSgerardnico $this->analyticsMetadata['age_modification'] = round((time() - $timestampModification) / 60 / 60 / 24); 519007225e5Sgerardnico 520007225e5Sgerardnico 521007225e5Sgerardnico /** 522007225e5Sgerardnico * Building the Top JSON in order 523007225e5Sgerardnico */ 524007225e5Sgerardnico global $ID; 5252c067407Sgerardnico $finalStats = array(); 5262c067407Sgerardnico $finalStats["id"] = $ID; 527c42a1196Sgerardnico $finalStats["date"] = date('Y-m-d H:i:s', time()); 528fa5961eaSgerardnico $finalStats['metadata'] = $this->analyticsMetadata; 529007225e5Sgerardnico ksort($statExport); 5302c067407Sgerardnico $finalStats[Analytics::STATISTICS] = $statExport; 5312c067407Sgerardnico $finalStats[Analytics::QUALITY] = $quality; // Quality after the sort to get them at the end 532007225e5Sgerardnico 533007225e5Sgerardnico 534007225e5Sgerardnico /** 535007225e5Sgerardnico * The result can be seen with 536007225e5Sgerardnico * doku.php?id=somepage&do=export_combo_analysis 5377c33ecc6Sgerardnico * 5387c33ecc6Sgerardnico * Set the header temporarily for the export.php file 539*85e82846SNickeau * 540*85e82846SNickeau * The mode in the export is 541007225e5Sgerardnico */ 542*85e82846SNickeau $mode = "combo_" . $this->getPluginComponent(); 5437c33ecc6Sgerardnico p_set_metadata( 5447c33ecc6Sgerardnico $ID, 545*85e82846SNickeau array("format" => array($mode => array("Content-Type" => 'application/json'))), 5467c33ecc6Sgerardnico false, 547*85e82846SNickeau true // Persistence is needed because there is a cache 5487c33ecc6Sgerardnico ); 5492c067407Sgerardnico $json_encoded = json_encode($finalStats, JSON_PRETTY_PRINT); 550007225e5Sgerardnico 5517c33ecc6Sgerardnico $this->page->saveAnalytics($finalStats); 552007225e5Sgerardnico $this->doc .= $json_encoded; 553007225e5Sgerardnico 554007225e5Sgerardnico } 555007225e5Sgerardnico 556007225e5Sgerardnico /** 557007225e5Sgerardnico */ 558007225e5Sgerardnico public function getFormat() 559007225e5Sgerardnico { 560531e725cSNickeau return self::RENDERER_FORMAT; 561007225e5Sgerardnico } 562007225e5Sgerardnico 563007225e5Sgerardnico public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') 564007225e5Sgerardnico { 565007225e5Sgerardnico 5669b9e6d1fSgerardnico $link = new LinkUtility($id); 5679b9e6d1fSgerardnico $link->setType(LinkUtility::TYPE_INTERNAL); 5689b9e6d1fSgerardnico $link->processLinkStats($this->stats); 569007225e5Sgerardnico 570007225e5Sgerardnico } 571007225e5Sgerardnico 572007225e5Sgerardnico public function externallink($url, $name = null) 573007225e5Sgerardnico { 574ef295d81Sgerardnico $link = new LinkUtility($url); 575ef295d81Sgerardnico $link->setType(LinkUtility::TYPE_EXTERNAL); 576ef295d81Sgerardnico if ($name != null) { 577ef295d81Sgerardnico $link->setName($name); 578ef295d81Sgerardnico } 579ef295d81Sgerardnico $link->processLinkStats($this->stats); 580007225e5Sgerardnico } 581007225e5Sgerardnico 582007225e5Sgerardnico public function header($text, $level, $pos) 583007225e5Sgerardnico { 584ebdc69ceSgerardnico if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) { 585ebdc69ceSgerardnico $this->stats[Analytics::HEADERS_COUNT] = []; 586ebdc69ceSgerardnico } 587ebdc69ceSgerardnico $heading = 'h' . $level; 588ebdc69ceSgerardnico if (!array_key_exists( 589ebdc69ceSgerardnico $heading, 590ebdc69ceSgerardnico $this->stats[Analytics::HEADERS_COUNT])) { 591ebdc69ceSgerardnico $this->stats[Analytics::HEADERS_COUNT][$heading] = 0; 592ebdc69ceSgerardnico } 593ebdc69ceSgerardnico $this->stats[Analytics::HEADERS_COUNT][$heading]++; 594ebdc69ceSgerardnico 595007225e5Sgerardnico $this->headerId++; 596ebdc69ceSgerardnico $this->stats[Analytics::HEADER_POSITION][$this->headerId] = $heading; 597ebdc69ceSgerardnico 598ebdc69ceSgerardnico /** 599ebdc69ceSgerardnico * Store the level of each heading 600ebdc69ceSgerardnico * They should only go from low to highest value 601ebdc69ceSgerardnico * for a good outline 602ebdc69ceSgerardnico */ 603ebdc69ceSgerardnico if (!array_key_exists(Analytics::HEADERS_COUNT, $this->stats)) { 604ebdc69ceSgerardnico $this->stats[self::HEADER_STRUCT] = []; 605ebdc69ceSgerardnico } 606ebdc69ceSgerardnico $this->stats[self::HEADER_STRUCT][] = $level; 607007225e5Sgerardnico 608007225e5Sgerardnico } 609007225e5Sgerardnico 610007225e5Sgerardnico public function smiley($smiley) 611007225e5Sgerardnico { 612007225e5Sgerardnico if ($smiley == 'FIXME') $this->stats[self::FIXME]++; 613007225e5Sgerardnico } 614007225e5Sgerardnico 615007225e5Sgerardnico public function linebreak() 616007225e5Sgerardnico { 617007225e5Sgerardnico if (!$this->tableopen) { 618007225e5Sgerardnico $this->stats['linebreak']++; 619007225e5Sgerardnico } 620007225e5Sgerardnico } 621007225e5Sgerardnico 622007225e5Sgerardnico public function table_open($maxcols = null, $numrows = null, $pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 623007225e5Sgerardnico { 624007225e5Sgerardnico $this->tableopen = true; 625007225e5Sgerardnico } 626007225e5Sgerardnico 627007225e5Sgerardnico public function table_close($pos = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 628007225e5Sgerardnico { 629007225e5Sgerardnico $this->tableopen = false; 630007225e5Sgerardnico } 631007225e5Sgerardnico 632007225e5Sgerardnico public function hr() 633007225e5Sgerardnico { 634007225e5Sgerardnico $this->stats['hr']++; 635007225e5Sgerardnico } 636007225e5Sgerardnico 637007225e5Sgerardnico public function quote_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 638007225e5Sgerardnico { 639007225e5Sgerardnico $this->stats['quote_count']++; 640007225e5Sgerardnico $this->quotelevel++; 641007225e5Sgerardnico $this->stats['quote_nest'] = max($this->quotelevel, $this->stats['quote_nest']); 642007225e5Sgerardnico } 643007225e5Sgerardnico 644007225e5Sgerardnico public function quote_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 645007225e5Sgerardnico { 646007225e5Sgerardnico $this->quotelevel--; 647007225e5Sgerardnico } 648007225e5Sgerardnico 649007225e5Sgerardnico public function strong_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 650007225e5Sgerardnico { 651007225e5Sgerardnico $this->formattingBracket++; 652007225e5Sgerardnico } 653007225e5Sgerardnico 654007225e5Sgerardnico public function strong_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 655007225e5Sgerardnico { 656007225e5Sgerardnico $this->formattingBracket--; 657007225e5Sgerardnico } 658007225e5Sgerardnico 659007225e5Sgerardnico public function emphasis_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 660007225e5Sgerardnico { 661007225e5Sgerardnico $this->formattingBracket++; 662007225e5Sgerardnico } 663007225e5Sgerardnico 664007225e5Sgerardnico public function emphasis_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 665007225e5Sgerardnico { 666007225e5Sgerardnico $this->formattingBracket--; 667007225e5Sgerardnico } 668007225e5Sgerardnico 669007225e5Sgerardnico public function underline_open() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 670007225e5Sgerardnico { 671007225e5Sgerardnico $this->formattingBracket++; 672007225e5Sgerardnico } 673007225e5Sgerardnico 674007225e5Sgerardnico public function underline_close() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 675007225e5Sgerardnico { 676007225e5Sgerardnico $this->formattingBracket--; 677007225e5Sgerardnico } 678007225e5Sgerardnico 679007225e5Sgerardnico public function cdata($text) 680007225e5Sgerardnico { 681007225e5Sgerardnico 682007225e5Sgerardnico /** 683007225e5Sgerardnico * It seems that you receive cdata 684007225e5Sgerardnico * when emphasis_open / underline_open / strong_open 685007225e5Sgerardnico * Stats are not for them 686007225e5Sgerardnico */ 687007225e5Sgerardnico if (!$this->formattingBracket) return; 688007225e5Sgerardnico 689007225e5Sgerardnico $this->plainTextId++; 690007225e5Sgerardnico 691007225e5Sgerardnico /** 692007225e5Sgerardnico * Length 693007225e5Sgerardnico */ 694007225e5Sgerardnico $len = strlen($text); 695007225e5Sgerardnico $this->stats[self::PLAINTEXT][$this->plainTextId]['len'] = $len; 696007225e5Sgerardnico 697007225e5Sgerardnico 698007225e5Sgerardnico /** 699007225e5Sgerardnico * Multi-formatting 700007225e5Sgerardnico */ 701007225e5Sgerardnico if ($this->formattingBracket > 1) { 702007225e5Sgerardnico $numberOfFormats = 1 * ($this->formattingBracket - 1); 703007225e5Sgerardnico $this->stats[self::PLAINTEXT][$this->plainTextId]['multiformat'] += $numberOfFormats; 704007225e5Sgerardnico } 705007225e5Sgerardnico 706007225e5Sgerardnico /** 707007225e5Sgerardnico * Total 708007225e5Sgerardnico */ 709007225e5Sgerardnico $this->stats[self::PLAINTEXT][0] += $len; 710007225e5Sgerardnico } 711007225e5Sgerardnico 712007225e5Sgerardnico public function internalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null) 713007225e5Sgerardnico { 714007225e5Sgerardnico $this->stats[Analytics::INTERNAL_MEDIAS_COUNT]++; 715007225e5Sgerardnico } 716007225e5Sgerardnico 717007225e5Sgerardnico public function externalmedia($src, $title = null, $align = null, $width = null, $height = null, $cache = null, $linking = null) 718007225e5Sgerardnico { 719007225e5Sgerardnico $this->stats[Analytics::EXTERNAL_MEDIAS]++; 720007225e5Sgerardnico } 721007225e5Sgerardnico 722007225e5Sgerardnico public function reset() 723007225e5Sgerardnico { 724007225e5Sgerardnico $this->stats = array(); 725fa5961eaSgerardnico $this->analyticsMetadata = array(); 726007225e5Sgerardnico $this->headerId = 0; 727007225e5Sgerardnico } 728007225e5Sgerardnico 729007225e5Sgerardnico public function setMeta($key, $value) 730007225e5Sgerardnico { 731fa5961eaSgerardnico $this->analyticsMetadata[$key] = $value; 732007225e5Sgerardnico } 733007225e5Sgerardnico 734007225e5Sgerardnico 735007225e5Sgerardnico} 736007225e5Sgerardnico 737