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