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