MediaWiki REL1_31
Cite.php
Go to the documentation of this file.
1<?php
2
28
33class Cite {
34
38 const DEFAULT_GROUP = '';
39
44 const MAX_STORAGE_LENGTH = 65535; // Size of MySQL 'blob' field
45
49 const EXT_DATA_KEY = 'Cite:References';
50
55
59 const CACHE_DURATION_ONPARSE = 3600; // 1 hour
60
64 const CACHE_DURATION_ONFETCH = 18000; // 5 hours
65
97 private $mRefs = [];
98
104 private $mOutCnt = 0;
105
109 private $mGroupCnt = [];
110
117 private $mCallCnt = 0;
118
127
133 private $mLinkLabels = [];
134
138 private $mParser;
139
146 private $mHaveAfterParse = false;
147
154 public $mInCite = false;
155
162 public $mInReferences = false;
163
169 private $mReferencesErrors = [];
170
176 private $mReferencesGroup = '';
177
185 private $mRefCallStack = [];
186
190 private $mBumpRefData = false;
191
196 private static $hooksInstalled = false;
197
208 public function ref( $str, array $argv, Parser $parser, PPFrame $frame ) {
209 if ( $this->mInCite ) {
210 return htmlspecialchars( "<ref>$str</ref>" );
211 }
212
213 $this->mCallCnt++;
214 $this->mInCite = true;
215
216 $ret = $this->guardedRef( $str, $argv, $parser );
217
218 $this->mInCite = false;
219
220 $parserOutput = $parser->getOutput();
221 $parserOutput->addModules( 'ext.cite.a11y' );
222 $parserOutput->addModuleStyles( 'ext.cite.styles' );
223
224 if ( is_callable( [ $frame, 'setVolatile' ] ) ) {
225 $frame->setVolatile();
226 }
227
228 // new <ref> tag, we may need to bump the ref data counter
229 // to avoid overwriting a previous group
230 $this->mBumpRefData = true;
231
232 return $ret;
233 }
234
244 private function guardedRef(
245 $str,
246 array $argv,
248 $default_group = self::DEFAULT_GROUP
249 ) {
250 $this->mParser = $parser;
251
252 # The key here is the "name" attribute.
253 list( $key, $group, $follow ) = $this->refArg( $argv );
254
255 # Split these into groups.
256 if ( $group === null ) {
257 if ( $this->mInReferences ) {
259 } else {
260 $group = $default_group;
261 }
262 }
263
264 /*
265 * This section deals with constructions of the form
266 *
267 * <references>
268 * <ref name="foo"> BAR </ref>
269 * </references>
270 */
271 if ( $this->mInReferences ) {
272 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
273 if ( $group != $this->mReferencesGroup ) {
274 # <ref> and <references> have conflicting group attributes.
275 $this->mReferencesErrors[] =
276 $this->error(
277 'cite_error_references_group_mismatch',
278 Sanitizer::safeEncodeAttribute( $group )
279 );
280 } elseif ( $str !== '' ) {
281 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
282 # Called with group attribute not defined in text.
283 $this->mReferencesErrors[] =
284 $this->error(
285 'cite_error_references_missing_group',
286 Sanitizer::safeEncodeAttribute( $group )
287 );
288 } elseif ( $key === null || $key === '' ) {
289 # <ref> calls inside <references> must be named
290 $this->mReferencesErrors[] =
291 $this->error( 'cite_error_references_no_key' );
292 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
293 # Called with name attribute not defined in text.
294 $this->mReferencesErrors[] =
295 $this->error( 'cite_error_references_missing_key', Sanitizer::safeEncodeAttribute( $key ) );
296 } else {
297 if (
298 isset( $this->mRefs[$group][$key]['text'] ) &&
299 $str !== $this->mRefs[$group][$key]['text']
300 ) {
301 // two refs with same key and different content
302 // add error message to the original ref
303 $this->mRefs[$group][$key]['text'] .= ' ' . $this->error(
304 'cite_error_references_duplicate_key', $key, 'noparse'
305 );
306 } else {
307 # Assign the text to corresponding ref
308 $this->mRefs[$group][$key]['text'] = $str;
309 }
310 }
311 } else {
312 # <ref> called in <references> has no content.
313 $this->mReferencesErrors[] =
314 $this->error( 'cite_error_empty_references_define', Sanitizer::safeEncodeAttribute( $key ) );
315 }
316 return '';
317 }
318
319 if ( $str === '' ) {
320 # <ref ...></ref>. This construct is invalid if
321 # it's a contentful ref, but OK if it's a named duplicate and should
322 # be equivalent <ref ... />, for compatability with #tag.
323 if ( is_string( $key ) && $key !== '' ) {
324 $str = null;
325 } else {
326 $this->mRefCallStack[] = false;
327
328 return $this->error( 'cite_error_ref_no_input' );
329 }
330 }
331
332 if ( $key === false ) {
333 # TODO: Comment this case; what does this condition mean?
334 $this->mRefCallStack[] = false;
335 return $this->error( 'cite_error_ref_too_many_keys' );
336 }
337
338 if ( $str === null && $key === null ) {
339 # Something like <ref />; this makes no sense.
340 $this->mRefCallStack[] = false;
341 return $this->error( 'cite_error_ref_no_key' );
342 }
343
344 if ( is_string( $key ) && preg_match( '/^[0-9]+$/', $key ) ||
345 is_string( $follow ) && preg_match( '/^[0-9]+$/', $follow )
346 ) {
347 # Numeric names mess up the resulting id's, potentially produ-
348 # cing duplicate id's in the XHTML. The Right Thing To Do
349 # would be to mangle them, but it's not really high-priority
350 # (and would produce weird id's anyway).
351
352 $this->mRefCallStack[] = false;
353 return $this->error( 'cite_error_ref_numeric_key' );
354 }
355
356 if ( preg_match(
357 '/<ref\b[^<]*?>/',
358 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
359 ) ) {
360 # (bug T8199) This most likely implies that someone left off the
361 # closing </ref> tag, which will cause the entire article to be
362 # eaten up until the next <ref>. So we bail out early instead.
363 # The fancy regex above first tries chopping out anything that
364 # looks like a comment or SGML tag, which is a crude way to avoid
365 # false alarms for <nowiki>, <pre>, etc.
366
367 # Possible improvement: print the warning, followed by the contents
368 # of the <ref> tag. This way no part of the article will be eaten
369 # even temporarily.
370
371 $this->mRefCallStack[] = false;
372 return $this->error( 'cite_error_included_ref' );
373 }
374
375 if ( is_string( $key ) || is_string( $str ) ) {
376 # We don't care about the content: if the key exists, the ref
377 # is presumptively valid. Either it stores a new ref, or re-
378 # fers to an existing one. If it refers to a nonexistent ref,
379 # we'll figure that out later. Likewise it's definitely valid
380 # if there's any content, regardless of key.
381
382 return $this->stack( $str, $key, $group, $follow, $argv );
383 }
384
385 # Not clear how we could get here, but something is probably
386 # wrong with the types. Let's fail fast.
387 throw new Exception( 'Invalid $str and/or $key: ' . serialize( [ $str, $key ] ) );
388 }
389
401 private function refArg( array $argv ) {
402 $cnt = count( $argv );
403 $group = null;
404 $key = null;
405 $follow = null;
406
407 if ( $cnt > 2 ) {
408 // There should only be one key or follow parameter, and one group parameter
409 // FIXME : this looks inconsistent, it should probably return a tuple
410 return false;
411 } elseif ( $cnt >= 1 ) {
412 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
413 return [ false, false, false ];
414 }
415 if ( isset( $argv['name'] ) ) {
416 // Key given.
417 $key = trim( $argv['name'] );
418 unset( $argv['name'] );
419 --$cnt;
420 }
421 if ( isset( $argv['follow'] ) ) {
422 // Follow given.
423 $follow = trim( $argv['follow'] );
424 unset( $argv['follow'] );
425 --$cnt;
426 }
427 if ( isset( $argv['group'] ) ) {
428 // Group given.
429 $group = $argv['group'];
430 unset( $argv['group'] );
431 --$cnt;
432 }
433
434 if ( $cnt === 0 ) {
435 return [ $key, $group, $follow ];
436 } else {
437 // Invalid key
438 return [ false, false, false ];
439 }
440 } else {
441 // No key
442 return [ null, $group, false ];
443 }
444 }
445
458 private function stack( $str, $key, $group, $follow, array $call ) {
459 if ( !isset( $this->mRefs[$group] ) ) {
460 $this->mRefs[$group] = [];
461 }
462 if ( !isset( $this->mGroupCnt[$group] ) ) {
463 $this->mGroupCnt[$group] = 0;
464 }
465 if ( $follow != null ) {
466 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
467 // add text to the note that is being followed
468 $this->mRefs[$group][$follow]['text'] .= ' ' . $str;
469 } else {
470 // insert part of note at the beginning of the group
471 $groupsCount = count( $this->mRefs[$group] );
472 for ( $k = 0; $k < $groupsCount; $k++ ) {
473 if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
474 break;
475 }
476 }
477 array_splice( $this->mRefs[$group], $k, 0, [ [
478 'count' => -1,
479 'text' => $str,
480 'key' => ++$this->mOutCnt,
481 'follow' => $follow
482 ] ] );
483 array_splice( $this->mRefCallStack, $k, 0,
484 [ [ 'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
485 }
486 // return an empty string : this is not a reference
487 return '';
488 }
489
490 if ( $key === null ) {
491 // No key
492 // $this->mRefs[$group][] = $str;
493 $this->mRefs[$group][] = [
494 'count' => -1,
495 'text' => $str,
496 'key' => ++$this->mOutCnt
497 ];
498 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
499
500 return $this->linkRef( $group, $this->mOutCnt );
501 }
502 if ( !is_string( $key ) ) {
503 throw new Exception( 'Invalid stack key: ' . serialize( $key ) );
504 }
505
506 // Valid key
507 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
508 // First occurrence
509 $this->mRefs[$group][$key] = [
510 'text' => $str,
511 'count' => 0,
512 'key' => ++$this->mOutCnt,
513 'number' => ++$this->mGroupCnt[$group]
514 ];
515 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
516
517 return $this->linkRef(
518 $group,
519 $key,
520 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
521 $this->mRefs[$group][$key]['number'],
522 "-" . $this->mRefs[$group][$key]['key']
523 );
524 }
525
526 // We've been here before
527 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
528 // If no text found before, use this text
529 $this->mRefs[$group][$key]['text'] = $str;
530 $this->mRefCallStack[] = [ 'assign', $call, $str, $key, $group,
531 $this->mRefs[$group][$key]['key'] ];
532 } else {
533 if ( $str != null && $str !== '' && $str !== $this->mRefs[$group][$key]['text'] ) {
534 // two refs with same key and different content
535 // add error message to the original ref
536 $this->mRefs[$group][$key]['text'] .= ' ' . $this->error(
537 'cite_error_references_duplicate_key', $key, 'noparse'
538 );
539 }
540 $this->mRefCallStack[] = [ 'increment', $call, $str, $key, $group,
541 $this->mRefs[$group][$key]['key'] ];
542 }
543 return $this->linkRef(
544 $group,
545 $key,
546 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
547 $this->mRefs[$group][$key]['number'],
548 "-" . $this->mRefs[$group][$key]['key']
549 );
550 }
551
573 private function rollbackRef( $type, $key, $group, $index ) {
574 if ( !isset( $this->mRefs[$group] ) ) {
575 return;
576 }
577
578 if ( $key === null ) {
579 foreach ( $this->mRefs[$group] as $k => $v ) {
580 if ( $this->mRefs[$group][$k]['key'] === $index ) {
581 $key = $k;
582 break;
583 }
584 }
585 }
586
587 // Sanity checks that specified element exists.
588 if ( $key === null ) {
589 return;
590 }
591 if ( !isset( $this->mRefs[$group][$key] ) ) {
592 return;
593 }
594 if ( $this->mRefs[$group][$key]['key'] != $index ) {
595 return;
596 }
597
598 switch ( $type ) {
599 case 'new':
600 # Rollback the addition of new elements to the stack.
601 unset( $this->mRefs[$group][$key] );
602 if ( $this->mRefs[$group] === [] ) {
603 unset( $this->mRefs[$group] );
604 unset( $this->mGroupCnt[$group] );
605 }
606 break;
607 case 'assign':
608 # Rollback assignment of text to pre-existing elements.
609 $this->mRefs[$group][$key]['text'] = null;
610 # continue without break
611 case 'increment':
612 # Rollback increase in named ref occurrences.
613 $this->mRefs[$group][$key]['count']--;
614 break;
615 }
616 }
617
628 public function references( $str, array $argv, Parser $parser, PPFrame $frame ) {
629 if ( $this->mInCite || $this->mInReferences ) {
630 if ( is_null( $str ) ) {
631 return htmlspecialchars( "<references/>" );
632 }
633 return htmlspecialchars( "<references>$str</references>" );
634 }
635 $this->mCallCnt++;
636 $this->mInReferences = true;
637 $ret = $this->guardedReferences( $str, $argv, $parser );
638 $this->mInReferences = false;
639 if ( is_callable( [ $frame, 'setVolatile' ] ) ) {
640 $frame->setVolatile();
641 }
642 return $ret;
643 }
644
653 private function guardedReferences(
654 $str,
655 array $argv,
657 $group = self::DEFAULT_GROUP
658 ) {
659 global $wgCiteResponsiveReferences;
660
661 $this->mParser = $parser;
662
663 if ( isset( $argv['group'] ) ) {
664 $group = $argv['group'];
665 unset( $argv['group'] );
666 }
667
668 if ( strval( $str ) !== '' ) {
669 $this->mReferencesGroup = $group;
670
671 # Detect whether we were sent already rendered <ref>s.
672 # Mostly a side effect of using #tag to call references.
673 # The following assumes that the parsed <ref>s sent within
674 # the <references> block were the most recent calls to
675 # <ref>. This assumption is true for all known use cases,
676 # but not strictly enforced by the parser. It is possible
677 # that some unusual combination of #tag, <references> and
678 # conditional parser functions could be created that would
679 # lead to malformed references here.
680 $count = substr_count( $str, Parser::MARKER_PREFIX . "-ref-" );
681 $redoStack = [];
682
683 # Undo effects of calling <ref> while unaware of containing <references>
684 for ( $i = 1; $i <= $count; $i++ ) {
685 if ( !$this->mRefCallStack ) {
686 break;
687 }
688
689 $call = array_pop( $this->mRefCallStack );
690 $redoStack[] = $call;
691 if ( $call !== false ) {
692 list( $type, $ref_argv, $ref_str,
693 $ref_key, $ref_group, $ref_index ) = $call;
694 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
695 }
696 }
697
698 # Rerun <ref> call now that mInReferences is set.
699 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
700 $call = $redoStack[$i];
701 if ( $call !== false ) {
702 list( $type, $ref_argv, $ref_str,
703 $ref_key, $ref_group, $ref_index ) = $call;
704 $this->guardedRef( $ref_str, $ref_argv, $parser );
705 }
706 }
707
708 # Parse $str to process any unparsed <ref> tags.
709 $parser->recursiveTagParse( $str );
710
711 # Reset call stack
712 $this->mRefCallStack = [];
713 }
714
715 if ( isset( $argv['responsive'] ) ) {
716 $responsive = $argv['responsive'] !== '0';
717 unset( $argv['responsive'] );
718 } else {
719 $responsive = $wgCiteResponsiveReferences;
720 }
721
722 // There are remaining parameters we don't recognise
723 if ( $argv ) {
724 return $this->error( 'cite_error_references_invalid_parameters' );
725 }
726
727 $s = $this->referencesFormat( $group, $responsive );
728
729 # Append errors generated while processing <references>
730 if ( $this->mReferencesErrors ) {
731 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
732 $this->mReferencesErrors = [];
733 }
734 return $s;
735 }
736
744 private function referencesFormat( $group, $responsive ) {
745 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
746 return '';
747 }
748
749 $ent = [];
750 foreach ( $this->mRefs[$group] as $k => $v ) {
751 $ent[] = $this->referencesFormatEntry( $k, $v );
752 }
753
754 // Add new lines between the list items (ref entires) to avoid confusing tidy (bug 13073).
755 // Note: This builds a string of wikitext, not html.
756 $parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ],
757 "\n" . implode( "\n", $ent ) . "\n"
758 );
759
760 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
761 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" );
762
763 if ( $responsive ) {
764 // Use a DIV wrap because column-count on a list directly is broken in Chrome.
765 // See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
766 $wrapClasses = [ 'mw-references-wrap' ];
767 if ( count( $this->mRefs[$group] ) > 10 ) {
768 $wrapClasses[] = 'mw-references-columns';
769 }
770 $ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
771 }
772
773 if ( !$this->mParser->getOptions()->getIsPreview() ) {
774 // save references data for later use by LinksUpdate hooks
775 $this->saveReferencesData( $group );
776 }
777
778 // done, clean up so we can reuse the group
779 unset( $this->mRefs[$group] );
780 unset( $this->mGroupCnt[$group] );
781
782 return $ret;
783 }
784
793 private function referencesFormatEntry( $key, $val ) {
794 // Anonymous reference
795 if ( !is_array( $val ) ) {
796 return wfMessage(
797 'cite_references_link_one',
798 $this->normalizeKey(
799 self::getReferencesKey( $key )
800 ),
801 $this->normalizeKey(
802 $this->refKey( $key )
803 ),
804 $this->referenceText( $key, $val )
805 )->inContentLanguage()->plain();
806 }
807 $text = $this->referenceText( $key, $val['text'] );
808 if ( isset( $val['follow'] ) ) {
809 return wfMessage(
810 'cite_references_no_link',
811 $this->normalizeKey(
812 self::getReferencesKey( $val['follow'] )
813 ),
814 $text
815 )->inContentLanguage()->plain();
816 }
817 if ( !isset( $val['count'] ) ) {
818 // this handles the case of section preview for list-defined references
819 return wfMessage( 'cite_references_link_many',
820 $this->normalizeKey(
821 self::getReferencesKey( $key . "-" . ( isset( $val['key'] ) ? $val['key'] : '' ) )
822 ),
823 '',
824 $text
825 )->inContentLanguage()->plain();
826 }
827 if ( $val['count'] < 0 ) {
828 return wfMessage(
829 'cite_references_link_one',
830 $this->normalizeKey(
831 self::getReferencesKey( $val['key'] )
832 ),
833 $this->normalizeKey(
834 # $this->refKey( $val['key'], $val['count'] )
835 $this->refKey( $val['key'] )
836 ),
837 $text
838 )->inContentLanguage()->plain();
839 // Standalone named reference, I want to format this like an
840 // anonymous reference because displaying "1. 1.1 Ref text" is
841 // overkill and users frequently use named references when they
842 // don't need them for convenience
843 }
844 if ( $val['count'] === 0 ) {
845 return wfMessage(
846 'cite_references_link_one',
847 $this->normalizeKey(
848 self::getReferencesKey( $key . "-" . $val['key'] )
849 ),
850 $this->normalizeKey(
851 # $this->refKey( $key, $val['count'] ),
852 $this->refKey( $key, $val['key'] . "-" . $val['count'] )
853 ),
854 $text
855 )->inContentLanguage()->plain();
856 // Named references with >1 occurrences
857 }
858 $links = [];
859 // for group handling, we have an extra key here.
860 for ( $i = 0; $i <= $val['count']; ++$i ) {
861 $links[] = wfMessage(
862 'cite_references_link_many_format',
863 $this->normalizeKey(
864 $this->refKey( $key, $val['key'] . "-$i" )
865 ),
866 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
868 )->inContentLanguage()->plain();
869 }
870
871 $list = $this->listToText( $links );
872
873 return wfMessage( 'cite_references_link_many',
874 $this->normalizeKey(
875 self::getReferencesKey( $key . "-" . $val['key'] )
876 ),
877 $list,
878 $text
879 )->inContentLanguage()->plain();
880 }
881
888 private function referenceText( $key, $text ) {
889 if ( !isset( $text ) || $text === '' ) {
890 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
891 return $this->warning( 'cite_warning_sectionpreview_no_text', $key, 'noparse' );
892 }
893 return $this->error( 'cite_error_references_no_text', $key, 'noparse' );
894 }
895 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
896 }
897
910 private function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
911 global $wgContLang;
912 $scope = strlen( $max );
913 $ret = $wgContLang->formatNum(
914 sprintf( "%s.%0{$scope}s", $base, $offset )
915 );
916 return $ret;
917 }
918
930 if ( !isset( $this->mBacklinkLabels ) ) {
931 $this->genBacklinkLabels();
932 }
933 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
934 return $this->mBacklinkLabels[$offset];
935 } else {
936 // Feed me!
937 return $this->error( 'cite_error_references_no_backlink_label', null, 'noparse' );
938 }
939 }
940
953 private function getLinkLabel( $offset, $group, $label ) {
954 $message = "cite_link_label_group-$group";
955 if ( !isset( $this->mLinkLabels[$group] ) ) {
956 $this->genLinkLabels( $group, $message );
957 }
958 if ( $this->mLinkLabels[$group] === false ) {
959 // Use normal representation, ie. "$group 1", "$group 2"...
960 return $label;
961 }
962
963 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
964 return $this->mLinkLabels[$group][$offset - 1];
965 } else {
966 // Feed me!
967 return $this->error( 'cite_error_no_link_label_group', [ $group, $message ], 'noparse' );
968 }
969 }
970
982 private function refKey( $key, $num = null ) {
983 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text();
984 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text();
985 if ( isset( $num ) ) {
986 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num )
987 ->inContentLanguage()->plain();
988 }
989
990 return "$prefix$key$suffix";
991 }
992
1003 public static function getReferencesKey( $key ) {
1004 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text();
1005 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text();
1006
1007 return "$prefix$key$suffix";
1008 }
1009
1025 private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
1026 global $wgContLang;
1027 $label = is_null( $label ) ? ++$this->mGroupCnt[$group] : $label;
1028
1029 return $this->mParser->recursiveTagParse(
1030 wfMessage(
1031 'cite_reference_link',
1032 $this->normalizeKey(
1033 $this->refKey( $key, $count )
1034 ),
1035 $this->normalizeKey(
1036 self::getReferencesKey( $key . $subkey )
1037 ),
1038 Sanitizer::safeEncodeAttribute(
1039 $this->getLinkLabel( $label, $group,
1040 ( ( $group === self::DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) )
1041 )
1042 )->inContentLanguage()->plain()
1043 );
1044 }
1045
1052 private function normalizeKey( $key ) {
1053 $key = Sanitizer::escapeIdForAttribute( $key );
1054 $key = Sanitizer::safeEncodeAttribute( $key );
1055
1056 return $key;
1057 }
1058
1071 private function listToText( $arr ) {
1072 $cnt = count( $arr );
1073
1074 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain();
1075 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain();
1076
1077 if ( $cnt === 1 ) {
1078 // Enforce always returning a string
1079 return (string)$arr[0];
1080 } else {
1081 $t = array_slice( $arr, 0, $cnt - 1 );
1082 return implode( $sep, $t ) . $and . $arr[$cnt - 1];
1083 }
1084 }
1085
1091 private function genBacklinkLabels() {
1092 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' )
1093 ->inContentLanguage()->plain();
1094 $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text );
1095 }
1096
1105 private function genLinkLabels( $group, $message ) {
1106 $text = false;
1107 $msg = wfMessage( $message )->inContentLanguage();
1108 if ( $msg->exists() ) {
1109 $text = $msg->plain();
1110 }
1111 $this->mLinkLabels[$group] = ( !$text ) ? false : preg_split( '#[\n\t ]#', $text );
1112 }
1113
1122 public function clearState( Parser &$parser ) {
1123 if ( $parser->extCite !== $this ) {
1124 return $parser->extCite->clearState( $parser );
1125 }
1126
1127 # Don't clear state when we're in the middle of parsing
1128 # a <ref> tag
1129 if ( $this->mInCite || $this->mInReferences ) {
1130 return true;
1131 }
1132
1133 $this->mGroupCnt = [];
1134 $this->mOutCnt = 0;
1135 $this->mCallCnt = 0;
1136 $this->mRefs = [];
1137 $this->mReferencesErrors = [];
1138 $this->mRefCallStack = [];
1139
1140 return true;
1141 }
1142
1150 public function cloneState( Parser $parser ) {
1151 if ( $parser->extCite !== $this ) {
1152 return $parser->extCite->cloneState( $parser );
1153 }
1154
1155 $parser->extCite = clone $this;
1156 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1157 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1158
1159 // Clear the state, making sure it will actually work.
1160 $parser->extCite->mInCite = false;
1161 $parser->extCite->mInReferences = false;
1162 $parser->extCite->clearState( $parser );
1163
1164 return true;
1165 }
1166
1181 public function checkRefsNoReferences( $afterParse, &$parser, &$text ) {
1182 global $wgCiteResponsiveReferences;
1183 if ( is_null( $parser->extCite ) ) {
1184 return true;
1185 }
1186 if ( $parser->extCite !== $this ) {
1187 return $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1188 }
1189
1190 if ( $afterParse ) {
1191 $this->mHaveAfterParse = true;
1192 } elseif ( $this->mHaveAfterParse ) {
1193 return true;
1194 }
1195
1196 if ( !$parser->getOptions()->getIsPreview() ) {
1197 // save references data for later use by LinksUpdate hooks
1198 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1199 $this->saveReferencesData();
1200 }
1201 $isSectionPreview = false;
1202 } else {
1203 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1204 }
1205
1206 $s = '';
1207 foreach ( $this->mRefs as $group => $refs ) {
1208 if ( !$refs ) {
1209 continue;
1210 }
1211 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1212 $s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
1213 } else {
1214 $s .= "\n<br />" .
1215 $this->error(
1216 'cite_error_group_refs_without_references',
1217 Sanitizer::safeEncodeAttribute( $group )
1218 );
1219 }
1220 }
1221 if ( $isSectionPreview && $s !== '' ) {
1222 // provide a preview of references in its own section
1223 $text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
1224 $headerMsg = wfMessage( 'cite_section_preview_references' );
1225 if ( !$headerMsg->isDisabled() ) {
1226 $text .= '<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1227 . $headerMsg->escaped()
1228 . '</h2>';
1229 }
1230 $text .= $s . '</div>';
1231 } else {
1232 $text .= $s;
1233 }
1234 return true;
1235 }
1236
1244 private function saveReferencesData( $group = self::DEFAULT_GROUP ) {
1245 global $wgCiteStoreReferencesData;
1246 if ( !$wgCiteStoreReferencesData ) {
1247 return;
1248 }
1249 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1250 if ( $savedRefs === null ) {
1251 // Initialize array structure
1252 $savedRefs = [
1253 'refs' => [],
1254 'version' => self::DATA_VERSION_NUMBER,
1255 ];
1256 }
1257 if ( $this->mBumpRefData ) {
1258 // This handles pages with multiple <references/> tags with <ref> tags in between.
1259 // On those, a group can appear several times, so we need to avoid overwriting
1260 // a previous appearance.
1261 $savedRefs['refs'][] = [];
1262 $this->mBumpRefData = false;
1263 }
1264 $n = count( $savedRefs['refs'] ) - 1;
1265 // save group
1266 $savedRefs['refs'][$n][$group] = $this->mRefs[$group];
1267
1268 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1269 }
1270
1280 public function checkAnyCalls( &$output ) {
1281 global $wgParser;
1282 /* InlineEditor always uses $wgParser */
1283 return ( $wgParser->extCite->mCallCnt <= 0 );
1284 }
1285
1293 public static function setHooks( Parser $parser ) {
1294 global $wgHooks;
1295
1296 $parser->extCite = new self();
1297
1298 if ( !self::$hooksInstalled ) {
1299 $wgHooks['ParserClearState'][] = [ $parser->extCite, 'clearState' ];
1300 $wgHooks['ParserCloned'][] = [ $parser->extCite, 'cloneState' ];
1301 $wgHooks['ParserAfterParse'][] = [ $parser->extCite, 'checkRefsNoReferences', true ];
1302 $wgHooks['ParserBeforeTidy'][] = [ $parser->extCite, 'checkRefsNoReferences', false ];
1303 $wgHooks['InlineEditorPartialAfterParse'][] = [ $parser->extCite, 'checkAnyCalls' ];
1304 self::$hooksInstalled = true;
1305 }
1306 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1307 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1308
1309 return true;
1310 }
1311
1320 private function error( $key, $param = null, $parse = 'parse' ) {
1321 # For ease of debugging and because errors are rare, we
1322 # use the user language and split the parser cache.
1323 $lang = $this->mParser->getOptions()->getUserLangObj();
1324 $dir = $lang->getDir();
1325
1326 # We rely on the fact that PHP is okay with passing unused argu-
1327 # ments to functions. If $1 is not used in the message, wfMessage will
1328 # just ignore the extra parameter.
1329 $msg = wfMessage(
1330 'cite_error',
1331 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1332 )
1333 ->inLanguage( $lang )
1334 ->plain();
1335
1336 $this->mParser->addTrackingCategory( 'cite-tracking-category-cite-error' );
1337
1338 $ret = Html::rawElement(
1339 'span',
1340 [
1341 'class' => 'error mw-ext-cite-error',
1342 'lang' => $lang->getHtmlCode(),
1343 'dir' => $dir,
1344 ],
1345 $msg
1346 );
1347
1348 if ( $parse === 'parse' ) {
1349 $ret = $this->mParser->recursiveTagParse( $ret );
1350 }
1351
1352 return $ret;
1353 }
1354
1363 private function warning( $key, $param = null, $parse = 'parse' ) {
1364 # For ease of debugging and because errors are rare, we
1365 # use the user language and split the parser cache.
1366 $lang = $this->mParser->getOptions()->getUserLangObj();
1367 $dir = $lang->getDir();
1368
1369 # We rely on the fact that PHP is okay with passing unused argu-
1370 # ments to functions. If $1 is not used in the message, wfMessage will
1371 # just ignore the extra parameter.
1372 $msg = wfMessage(
1373 'cite_warning',
1374 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1375 )
1376 ->inLanguage( $lang )
1377 ->plain();
1378
1379 $key = preg_replace( '/^cite_warning_/', '', $key ) . '';
1380 $ret = Html::rawElement(
1381 'span',
1382 [
1383 'class' => 'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1384 Sanitizer::escapeClass( $key ),
1385 'lang' => $lang->getHtmlCode(),
1386 'dir' => $dir,
1387 ],
1388 $msg
1389 );
1390
1391 if ( $parse === 'parse' ) {
1392 $ret = $this->mParser->recursiveTagParse( $ret );
1393 }
1394
1395 return $ret;
1396 }
1397
1405 public static function getStoredReferences( Title $title ) {
1406 global $wgCiteStoreReferencesData;
1407 if ( !$wgCiteStoreReferencesData ) {
1408 return false;
1409 }
1410 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1411 $key = $cache->makeKey( self::EXT_DATA_KEY, $title->getArticleID() );
1412 return $cache->getWithSetCallback(
1413 $key,
1414 self::CACHE_DURATION_ONFETCH,
1415 function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
1416 $dbr = wfGetDB( DB_REPLICA );
1417 $setOpts += Database::getCacheSetOptions( $dbr );
1418 return self::recursiveFetchRefsFromDB( $title, $dbr );
1419 },
1420 [
1421 'checkKeys' => [ $key ],
1422 'lockTSE' => 30,
1423 ]
1424 );
1425 }
1426
1438 private static function recursiveFetchRefsFromDB( Title $title, IDatabase $dbr,
1439 $string = '', $i = 1 ) {
1440 $id = $title->getArticleID();
1441 $result = $dbr->selectField(
1442 'page_props',
1443 'pp_value',
1444 [
1445 'pp_page' => $id,
1446 'pp_propname' => 'references-' . $i
1447 ],
1448 __METHOD__
1449 );
1450 if ( $result !== false ) {
1451 $string .= $result;
1452 $decodedString = gzdecode( $string );
1453 if ( $decodedString !== false ) {
1454 $json = json_decode( $decodedString, true );
1455 if ( json_last_error() === JSON_ERROR_NONE ) {
1456 return $json;
1457 }
1458 // corrupted json ?
1459 // shouldn't happen since when string is truncated, gzdecode should fail
1460 wfDebug( "Corrupted json detected when retrieving stored references for title id $id" );
1461 }
1462 // if gzdecode fails, try to fetch next references- property value
1463 return self::recursiveFetchRefsFromDB( $title, $dbr, $string, ++$i );
1464
1465 } else {
1466 // no refs stored in page_props at this index
1467 if ( $i > 1 ) {
1468 // shouldn't happen
1469 wfDebug( "Failed to retrieve stored references for title id $id" );
1470 }
1471 return false;
1472 }
1473 }
1474
1475}
serialize()
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$wgParser
Definition Setup.php:917
global $argv
WARNING: MediaWiki core hardcodes this class name to check if the Cite extension is installed.
Definition Cite.php:33
const CACHE_DURATION_ONFETCH
Cache duration set when fetching references from db.
Definition Cite.php:64
const DATA_VERSION_NUMBER
Version number in case we change the data structure in the future.
Definition Cite.php:54
getLinkLabel( $offset, $group, $label)
Generate a custom format link for a group given an offset, e.g.
Definition Cite.php:953
guardedReferences( $str, array $argv, Parser $parser, $group=self::DEFAULT_GROUP)
Definition Cite.php:653
array $mLinkLabels
The links to use per group, in order.
Definition Cite.php:133
const EXT_DATA_KEY
Key used for storage in parser output's ExtensionData and ObjectCache.
Definition Cite.php:49
refArg(array $argv)
Parse the arguments to the <ref> tag.
Definition Cite.php:401
linkRef( $group, $key, $count=null, $label=null, $subkey='')
Generate a link (<sup ...) for the <ref> element from a key and return XHTML ready for output.
Definition Cite.php:1025
referenceText( $key, $text)
Returns formatted reference text.
Definition Cite.php:888
error( $key, $param=null, $parse='parse')
Return an error message based on an error ID.
Definition Cite.php:1320
const MAX_STORAGE_LENGTH
Maximum storage capacity for pp_value field of page_props table.
Definition Cite.php:44
genLinkLabels( $group, $message)
Generate the labels to pass to the 'cite_reference_link' message instead of numbers,...
Definition Cite.php:1105
genBacklinkLabels()
Generate the labels to pass to the 'cite_references_link_many_format' message, the format is an arbit...
Definition Cite.php:1091
references( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <references>
Definition Cite.php:628
ref( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <ref>
Definition Cite.php:208
Parser $mParser
Definition Cite.php:138
int $mCallCnt
Counter to track the total number of (useful) calls to either the ref or references tag hook.
Definition Cite.php:117
checkRefsNoReferences( $afterParse, &$parser, &$text)
Called at the end of page processing to append a default references section, if refs were used withou...
Definition Cite.php:1181
stack( $str, $key, $group, $follow, array $call)
Populate $this->mRefs based on input and arguments to <ref>
Definition Cite.php:458
string[] $mReferencesErrors
Error stack used when defining refs in <references>
Definition Cite.php:169
static Boolean $hooksInstalled
Did we install us into $wgHooks yet?
Definition Cite.php:196
saveReferencesData( $group=self::DEFAULT_GROUP)
Saves references in parser extension data This is called by each <references> tag,...
Definition Cite.php:1244
guardedRef( $str, array $argv, Parser $parser, $default_group=self::DEFAULT_GROUP)
Definition Cite.php:244
clearState(Parser &$parser)
Gets run when Parser::clearState() gets run, since we don't want the counts to transcend pages and ot...
Definition Cite.php:1122
const DEFAULT_GROUP
Definition Cite.php:38
listToText( $arr)
This does approximately the same thing as Language::listToText() but due to this being used for a sli...
Definition Cite.php:1071
string[] $mBacklinkLabels
The backlinks, in order, to pass as $3 to 'cite_references_link_many_format', defined in 'cite_refere...
Definition Cite.php:126
array $mRefCallStack
<ref> call stack Used to cleanup out of sequence ref calls created by #tag See description of functio...
Definition Cite.php:185
static getStoredReferences(Title $title)
Fetch references stored for the given title in page_props For performance, results are cached.
Definition Cite.php:1405
refKey( $key, $num=null)
Return an id for use in wikitext output based on a key and optionally the number of it,...
Definition Cite.php:982
const CACHE_DURATION_ONPARSE
Cache duration set when parsing a page with references.
Definition Cite.php:59
static recursiveFetchRefsFromDB(Title $title, IDatabase $dbr, $string='', $i=1)
Reconstructs compressed json by successively retrieving the properties references-1,...
Definition Cite.php:1438
boolean $mHaveAfterParse
True when the ParserAfterParse hook has been called.
Definition Cite.php:146
cloneState(Parser $parser)
Gets run when the parser is cloned.
Definition Cite.php:1150
referencesFormatEntryAlternateBacklinkLabel( $offset)
Generate a custom format backlink given an offset, e.g.
Definition Cite.php:929
referencesFormat( $group, $responsive)
Make output to be returned from the references() function.
Definition Cite.php:744
int $mOutCnt
Count for user displayed output (ref[1], ref[2], ...)
Definition Cite.php:104
int[] $mGroupCnt
Definition Cite.php:109
bool $mBumpRefData
Definition Cite.php:190
referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max)
Generate a numeric backlink given a base number and an offset, e.g.
Definition Cite.php:910
normalizeKey( $key)
Normalizes and sanitizes a reference key.
Definition Cite.php:1052
static setHooks(Parser $parser)
Initialize the parser hooks.
Definition Cite.php:1293
boolean $mInCite
True when a <ref> tag is being processed.
Definition Cite.php:154
static getReferencesKey( $key)
Return an id for use in wikitext output based on a key and optionally the number of it,...
Definition Cite.php:1003
boolean $mInReferences
True when a <references> tag is being processed.
Definition Cite.php:162
string $mReferencesGroup
Group used when in <references> block.
Definition Cite.php:176
referencesFormatEntry( $key, $val)
Format a single entry for the referencesFormat() function.
Definition Cite.php:793
array[] $mRefs
Datastructure representing <ref> input, in the format of: [ 'user supplied' => [ 'text' => 'user sup...
Definition Cite.php:97
rollbackRef( $type, $key, $group, $index)
Partially undoes the effect of calls to stack()
Definition Cite.php:573
checkAnyCalls(&$output)
Hook for the InlineEditor extension.
Definition Cite.php:1280
warning( $key, $param=null, $parse='parse')
Return a warning message based on a warning ID.
Definition Cite.php:1363
MediaWikiServices is the service locator for the application scope of MediaWiki.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:70
Represents a title within MediaWiki.
Definition Title.php:39
Relational database abstraction object.
Definition Database.php:48
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2603
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2255
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults error
Definition hooks.txt:2612
either a plain
Definition hooks.txt:2056
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2006
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
$wgHooks['ArticleShow'][]
Definition hooks.txt:108
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
setVolatile( $flag=true)
Set the "volatile" flag.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang