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