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