MediaWiki REL1_34
Cite.php
Go to the documentation of this file.
1<?php
2
28
29class Cite {
30
34 const DEFAULT_GROUP = '';
35
40 const MAX_STORAGE_LENGTH = 65535; // Size of MySQL 'blob' field
41
45 const EXT_DATA_KEY = 'Cite:References';
46
51
55 const CACHE_DURATION_ONPARSE = 3600; // 1 hour
56
60 const CACHE_DURATION_ONFETCH = 18000; // 5 hours
61
93 private $mRefs = [];
94
100 private $mOutCnt = 0;
101
105 private $mGroupCnt = [];
106
113 private $mCallCnt = 0;
114
123
129 private $mLinkLabels = [];
130
134 private $mParser;
135
142 private $mHaveAfterParse = false;
143
150 public $mInCite = false;
151
158 public $mInReferences = false;
159
165 private $mReferencesErrors = [];
166
172 private $mReferencesGroup = '';
173
181 private $mRefCallStack = [];
182
186 private $mBumpRefData = false;
187
192 private static $hooksInstalled = false;
193
204 public function ref( $str, array $argv, Parser $parser, PPFrame $frame ) {
205 if ( $this->mInCite ) {
206 return htmlspecialchars( "<ref>$str</ref>" );
207 }
208
209 $this->mCallCnt++;
210 $this->mInCite = true;
211
212 $ret = $this->guardedRef( $str, $argv, $parser );
213
214 $this->mInCite = false;
215
216 $parserOutput = $parser->getOutput();
217 $parserOutput->addModules( 'ext.cite.ux-enhancements' );
218 $parserOutput->addModuleStyles( 'ext.cite.styles' );
219
220 $frame->setVolatile();
221
222 // new <ref> tag, we may need to bump the ref data counter
223 // to avoid overwriting a previous group
224 $this->mBumpRefData = true;
225
226 return $ret;
227 }
228
237 private function guardedRef(
238 $str,
239 array $argv,
240 Parser $parser
241 ) {
242 $this->mParser = $parser;
243
244 # The key here is the "name" attribute.
245 list( $key, $group, $follow, $dir ) = $this->refArg( $argv );
246 // empty string indicate invalid dir
247 if ( $dir === '' && $str !== '' ) {
248 $str .= $this->plainError( 'cite_error_ref_invalid_dir', $argv['dir'] );
249 }
250 # Split these into groups.
251 if ( $group === null ) {
252 if ( $this->mInReferences ) {
254 } else {
255 $group = self::DEFAULT_GROUP;
256 }
257 }
258
259 /*
260 * This section deals with constructions of the form
261 *
262 * <references>
263 * <ref name="foo"> BAR </ref>
264 * </references>
265 */
266 if ( $this->mInReferences ) {
267 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
268 if ( $group != $this->mReferencesGroup ) {
269 # <ref> and <references> have conflicting group attributes.
270 $this->mReferencesErrors[] =
271 $this->error(
272 'cite_error_references_group_mismatch',
273 Sanitizer::safeEncodeAttribute( $group )
274 );
275 } elseif ( $str !== '' ) {
276 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
277 # Called with group attribute not defined in text.
278 $this->mReferencesErrors[] =
279 $this->error(
280 'cite_error_references_missing_group',
281 Sanitizer::safeEncodeAttribute( $group )
282 );
283 } elseif ( $key === null || $key === '' ) {
284 # <ref> calls inside <references> must be named
285 $this->mReferencesErrors[] =
286 $this->error( 'cite_error_references_no_key' );
287 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
288 # Called with name attribute not defined in text.
289 $this->mReferencesErrors[] =
290 $this->error( 'cite_error_references_missing_key', Sanitizer::safeEncodeAttribute( $key ) );
291 } else {
292 if (
293 isset( $this->mRefs[$group][$key]['text'] ) &&
294 $str !== $this->mRefs[$group][$key]['text']
295 ) {
296 // two refs with same key and different content
297 // add error message to the original ref
298 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
299 'cite_error_references_duplicate_key', $key
300 );
301 } else {
302 # Assign the text to corresponding ref
303 $this->mRefs[$group][$key]['text'] = $str;
304 }
305 }
306 } else {
307 # <ref> called in <references> has no content.
308 $this->mReferencesErrors[] =
309 $this->error( 'cite_error_empty_references_define', Sanitizer::safeEncodeAttribute( $key ) );
310 }
311 return '';
312 }
313
314 if ( $str === '' ) {
315 # <ref ...></ref>. This construct is invalid if
316 # it's a contentful ref, but OK if it's a named duplicate and should
317 # be equivalent <ref ... />, for compatability with #tag.
318 if ( is_string( $key ) && $key !== '' ) {
319 $str = null;
320 } else {
321 $this->mRefCallStack[] = false;
322
323 return $this->error( 'cite_error_ref_no_input' );
324 }
325 }
326
327 if ( $key === false ) {
328 # TODO: Comment this case; what does this condition mean?
329 $this->mRefCallStack[] = false;
330 return $this->error( 'cite_error_ref_too_many_keys' );
331 }
332
333 if ( $str === null && $key === null ) {
334 # Something like <ref />; this makes no sense.
335 $this->mRefCallStack[] = false;
336 return $this->error( 'cite_error_ref_no_key' );
337 }
338
339 if ( is_string( $key ) && preg_match( '/^\d+$/', $key ) ||
340 is_string( $follow ) && preg_match( '/^\d+$/', $follow )
341 ) {
342 # Numeric names mess up the resulting id's, potentially produ-
343 # cing duplicate id's in the XHTML. The Right Thing To Do
344 # would be to mangle them, but it's not really high-priority
345 # (and would produce weird id's anyway).
346
347 $this->mRefCallStack[] = false;
348 return $this->error( 'cite_error_ref_numeric_key' );
349 }
350
351 if ( preg_match(
352 '/<ref\b[^<]*?>/',
353 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
354 ) ) {
355 # (bug T8199) This most likely implies that someone left off the
356 # closing </ref> tag, which will cause the entire article to be
357 # eaten up until the next <ref>. So we bail out early instead.
358 # The fancy regex above first tries chopping out anything that
359 # looks like a comment or SGML tag, which is a crude way to avoid
360 # false alarms for <nowiki>, <pre>, etc.
361
362 # Possible improvement: print the warning, followed by the contents
363 # of the <ref> tag. This way no part of the article will be eaten
364 # even temporarily.
365
366 $this->mRefCallStack[] = false;
367 return $this->error( 'cite_error_included_ref' );
368 }
369
370 if ( is_string( $key ) || is_string( $str ) ) {
371 # We don't care about the content: if the key exists, the ref
372 # is presumptively valid. Either it stores a new ref, or re-
373 # fers to an existing one. If it refers to a nonexistent ref,
374 # we'll figure that out later. Likewise it's definitely valid
375 # if there's any content, regardless of key.
376
377 return $this->stack( $str, $key, $group, $follow, $argv, $dir, $parser );
378 }
379
380 # Not clear how we could get here, but something is probably
381 # wrong with the types. Let's fail fast.
382 throw new Exception( 'Invalid $str and/or $key: ' . serialize( [ $str, $key ] ) );
383 }
384
398 private function refArg( array $argv ) {
399 $group = null;
400 $key = null;
401 $follow = null;
402 $dir = null;
403
404 if ( isset( $argv['dir'] ) ) {
405 // compare the dir attribute value against an explicit whitelist.
406 $dir = '';
407 $isValidDir = in_array( strtolower( $argv['dir'] ), [ 'ltr', 'rtl' ] );
408 if ( $isValidDir ) {
409 $dir = Html::expandAttributes( [ 'class' => 'mw-cite-dir-' . strtolower( $argv['dir'] ) ] );
410 }
411
412 unset( $argv['dir'] );
413 }
414
415 if ( $argv === [] ) {
416 // No key
417 return [ null, null, false, $dir ];
418 }
419
420 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
421 return [ false, false, false, false ];
422 }
423
424 if ( isset( $argv['name'] ) ) {
425 // Key given.
426 $key = trim( $argv['name'] );
427 unset( $argv['name'] );
428 }
429 if ( isset( $argv['follow'] ) ) {
430 // Follow given.
431 $follow = trim( $argv['follow'] );
432 unset( $argv['follow'] );
433 }
434 if ( isset( $argv['group'] ) ) {
435 // Group given.
436 $group = $argv['group'];
437 unset( $argv['group'] );
438 }
439
440 if ( $argv !== [] ) {
441 // Invalid key
442 return [ false, false, false, false ];
443 }
444
445 return [ $key, $group, $follow, $dir ];
446 }
447
462 private function stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser ) {
463 if ( !isset( $this->mRefs[$group] ) ) {
464 $this->mRefs[$group] = [];
465 }
466 if ( !isset( $this->mGroupCnt[$group] ) ) {
467 $this->mGroupCnt[$group] = 0;
468 }
469 if ( $follow != null ) {
470 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
471 // add text to the note that is being followed
472 $this->mRefs[$group][$follow]['text'] .= ' ' . $str;
473 } else {
474 // insert part of note at the beginning of the group
475 $groupsCount = count( $this->mRefs[$group] );
476 for ( $k = 0; $k < $groupsCount; $k++ ) {
477 if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
478 break;
479 }
480 }
481 array_splice( $this->mRefs[$group], $k, 0, [ [
482 'count' => -1,
483 'text' => $str,
484 'key' => ++$this->mOutCnt,
485 'follow' => $follow,
486 'dir' => $dir
487 ] ] );
488 array_splice( $this->mRefCallStack, $k, 0,
489 [ [ 'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
490 }
491 // return an empty string : this is not a reference
492 return '';
493 }
494
495 if ( $key === null ) {
496 // No key
497 // $this->mRefs[$group][] = $str;
498
499 $this->mRefs[$group][] = [
500 'count' => -1,
501 'text' => $str,
502 'key' => ++$this->mOutCnt,
503 'dir' => $dir
504 ];
505 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
506
507 return $this->linkRef( $group, $this->mOutCnt );
508 }
509 if ( !is_string( $key ) ) {
510 throw new Exception( 'Invalid stack key: ' . serialize( $key ) );
511 }
512
513 // Valid key
514 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
515 // First occurrence
516 $this->mRefs[$group][$key] = [
517 'text' => $str,
518 'count' => 0,
519 'key' => ++$this->mOutCnt,
520 'number' => ++$this->mGroupCnt[$group],
521 'dir' => $dir
522 ];
523 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
524
525 return $this->linkRef(
526 $group,
527 $key,
528 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
529 $this->mRefs[$group][$key]['number'],
530 "-" . $this->mRefs[$group][$key]['key']
531 );
532 }
533
534 // We've been here before
535 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
536 // If no text found before, use this text
537 $this->mRefs[$group][$key]['text'] = $str;
538 // Use the dir parameter only from the full definition of a named ref tag
539 $this->mRefs[$group][$key]['dir'] = $dir;
540 $this->mRefCallStack[] = [ 'assign', $call, $str, $key, $group,
541 $this->mRefs[$group][$key]['key'] ];
542 } else {
543 if ( $str != null && $str !== ''
544 // T205803 different strip markers might hide the same text
545 && $parser->mStripState->unstripBoth( $str )
546 !== $parser->mStripState->unstripBoth( $this->mRefs[$group][$key]['text'] )
547 ) {
548 // two refs with same key and different content
549 // add error message to the original ref
550 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
551 'cite_error_references_duplicate_key', $key
552 );
553 }
554 $this->mRefCallStack[] = [ 'increment', $call, $str, $key, $group,
555 $this->mRefs[$group][$key]['key'] ];
556 }
557 return $this->linkRef(
558 $group,
559 $key,
560 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
561 $this->mRefs[$group][$key]['number'],
562 "-" . $this->mRefs[$group][$key]['key']
563 );
564 }
565
587 private function rollbackRef( $type, $key, $group, $index ) {
588 if ( !isset( $this->mRefs[$group] ) ) {
589 return;
590 }
591
592 if ( $key === null ) {
593 foreach ( $this->mRefs[$group] as $k => $v ) {
594 if ( $this->mRefs[$group][$k]['key'] === $index ) {
595 $key = $k;
596 break;
597 }
598 }
599 }
600
601 // Sanity checks that specified element exists.
602 if ( $key === null ) {
603 return;
604 }
605 if ( !isset( $this->mRefs[$group][$key] ) ) {
606 return;
607 }
608 if ( $this->mRefs[$group][$key]['key'] != $index ) {
609 return;
610 }
611
612 switch ( $type ) {
613 case 'new':
614 # Rollback the addition of new elements to the stack.
615 unset( $this->mRefs[$group][$key] );
616 if ( $this->mRefs[$group] === [] ) {
617 unset( $this->mRefs[$group] );
618 unset( $this->mGroupCnt[$group] );
619 }
620 break;
621 case 'assign':
622 # Rollback assignment of text to pre-existing elements.
623 $this->mRefs[$group][$key]['text'] = null;
624 # continue without break
625 case 'increment':
626 # Rollback increase in named ref occurrences.
627 $this->mRefs[$group][$key]['count']--;
628 break;
629 }
630 }
631
642 public function references( $str, array $argv, Parser $parser, PPFrame $frame ) {
643 if ( $this->mInCite || $this->mInReferences ) {
644 if ( $str === null ) {
645 return htmlspecialchars( "<references/>" );
646 }
647 return htmlspecialchars( "<references>$str</references>" );
648 }
649 $this->mCallCnt++;
650 $this->mInReferences = true;
651 $ret = $this->guardedReferences( $str, $argv, $parser );
652 $this->mInReferences = false;
653 $frame->setVolatile();
654 return $ret;
655 }
656
666 private function guardedReferences(
667 $str,
668 array $argv,
669 Parser $parser
670 ) {
671 global $wgCiteResponsiveReferences;
672
673 $this->mParser = $parser;
674
675 if ( isset( $argv['group'] ) ) {
676 $group = $argv['group'];
677 unset( $argv['group'] );
678 } else {
679 $group = self::DEFAULT_GROUP;
680 }
681
682 if ( strval( $str ) !== '' ) {
683 $this->mReferencesGroup = $group;
684
685 # Detect whether we were sent already rendered <ref>s.
686 # Mostly a side effect of using #tag to call references.
687 # The following assumes that the parsed <ref>s sent within
688 # the <references> block were the most recent calls to
689 # <ref>. This assumption is true for all known use cases,
690 # but not strictly enforced by the parser. It is possible
691 # that some unusual combination of #tag, <references> and
692 # conditional parser functions could be created that would
693 # lead to malformed references here.
694 $count = substr_count( $str, Parser::MARKER_PREFIX . "-ref-" );
695 $redoStack = [];
696
697 # Undo effects of calling <ref> while unaware of containing <references>
698 for ( $i = 1; $i <= $count; $i++ ) {
699 if ( !$this->mRefCallStack ) {
700 break;
701 }
702
703 $call = array_pop( $this->mRefCallStack );
704 $redoStack[] = $call;
705 if ( $call !== false ) {
706 list( $type, $ref_argv, $ref_str,
707 $ref_key, $ref_group, $ref_index ) = $call;
708 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
709 }
710 }
711
712 # Rerun <ref> call now that mInReferences is set.
713 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
714 $call = $redoStack[$i];
715 if ( $call !== false ) {
716 list( $type, $ref_argv, $ref_str,
717 $ref_key, $ref_group, $ref_index ) = $call;
718 $this->guardedRef( $ref_str, $ref_argv, $parser );
719 }
720 }
721
722 # Parse $str to process any unparsed <ref> tags.
723 $parser->recursiveTagParse( $str );
724
725 # Reset call stack
726 $this->mRefCallStack = [];
727 }
728
729 if ( isset( $argv['responsive'] ) ) {
730 $responsive = $argv['responsive'] !== '0';
731 unset( $argv['responsive'] );
732 } else {
733 $responsive = $wgCiteResponsiveReferences;
734 }
735
736 // There are remaining parameters we don't recognise
737 if ( $argv ) {
738 return $this->error( 'cite_error_references_invalid_parameters' );
739 }
740
741 $s = $this->referencesFormat( $group, $responsive );
742
743 # Append errors generated while processing <references>
744 if ( $this->mReferencesErrors ) {
745 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
746 $this->mReferencesErrors = [];
747 }
748 return $s;
749 }
750
761 private function referencesFormat( $group, $responsive ) {
762 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
763 return '';
764 }
765
766 $ent = [];
767 foreach ( $this->mRefs[$group] as $k => $v ) {
768 $ent[] = $this->referencesFormatEntry( $k, $v );
769 }
770
771 // Add new lines between the list items (ref entires) to avoid confusing tidy (T15073).
772 // Note: This builds a string of wikitext, not html.
773 $parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ],
774 "\n" . implode( "\n", $ent ) . "\n"
775 );
776
777 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
778 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" );
779
780 if ( $responsive ) {
781 // Use a DIV wrap because column-count on a list directly is broken in Chrome.
782 // See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
783 $wrapClasses = [ 'mw-references-wrap' ];
784 if ( count( $this->mRefs[$group] ) > 10 ) {
785 $wrapClasses[] = 'mw-references-columns';
786 }
787 $ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
788 }
789
790 if ( !$this->mParser->getOptions()->getIsPreview() ) {
791 // save references data for later use by LinksUpdate hooks
792 $this->saveReferencesData( $group );
793 }
794
795 // done, clean up so we can reuse the group
796 unset( $this->mRefs[$group] );
797 unset( $this->mGroupCnt[$group] );
798
799 return $ret;
800 }
801
810 private function referencesFormatEntry( $key, $val ) {
811 // Anonymous reference
812 if ( !is_array( $val ) ) {
813 return wfMessage(
814 'cite_references_link_one',
815 $this->normalizeKey(
816 self::getReferencesKey( $key )
817 ),
818 $this->normalizeKey(
819 $this->refKey( $key )
820 ),
821 $this->referenceText( $key, $val ),
822 $val['dir']
823 )->inContentLanguage()->plain();
824 }
825 $text = $this->referenceText( $key, $val['text'] );
826 if ( isset( $val['follow'] ) ) {
827 return wfMessage(
828 'cite_references_no_link',
829 $this->normalizeKey(
830 self::getReferencesKey( $val['follow'] )
831 ),
832 $text
833 )->inContentLanguage()->plain();
834 }
835 if ( !isset( $val['count'] ) ) {
836 // this handles the case of section preview for list-defined references
837 return wfMessage( 'cite_references_link_many',
838 $this->normalizeKey(
839 self::getReferencesKey( $key . "-" . ( $val['key'] ?? '' ) )
840 ),
841 '',
842 $text
843 )->inContentLanguage()->plain();
844 }
845 if ( $val['count'] < 0 ) {
846 return wfMessage(
847 'cite_references_link_one',
848 $this->normalizeKey(
849 self::getReferencesKey( $val['key'] )
850 ),
851 $this->normalizeKey(
852 # $this->refKey( $val['key'], $val['count'] )
853 $this->refKey( $val['key'] )
854 ),
855 $text,
856 $val['dir']
857 )->inContentLanguage()->plain();
858 // Standalone named reference, I want to format this like an
859 // anonymous reference because displaying "1. 1.1 Ref text" is
860 // overkill and users frequently use named references when they
861 // don't need them for convenience
862 }
863 if ( $val['count'] === 0 ) {
864 return wfMessage(
865 'cite_references_link_one',
866 $this->normalizeKey(
867 self::getReferencesKey( $key . "-" . $val['key'] )
868 ),
869 $this->normalizeKey(
870 # $this->refKey( $key, $val['count'] ),
871 $this->refKey( $key, $val['key'] . "-" . $val['count'] )
872 ),
873 $text,
874 $val['dir']
875 )->inContentLanguage()->plain();
876 // Named references with >1 occurrences
877 }
878 $links = [];
879 // for group handling, we have an extra key here.
880 for ( $i = 0; $i <= $val['count']; ++$i ) {
881 $links[] = wfMessage(
882 'cite_references_link_many_format',
883 $this->normalizeKey(
884 $this->refKey( $key, $val['key'] . "-$i" )
885 ),
886 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
888 )->inContentLanguage()->plain();
889 }
890
891 $list = $this->listToText( $links );
892
893 return wfMessage( 'cite_references_link_many',
894 $this->normalizeKey(
895 self::getReferencesKey( $key . "-" . $val['key'] )
896 ),
897 $list,
898 $text,
899 $val['dir']
900 )->inContentLanguage()->plain();
901 }
902
909 private function referenceText( $key, $text ) {
910 if ( $text === null || $text === '' ) {
911 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
912 return $this->warning( 'cite_warning_sectionpreview_no_text', $key, 'noparse' );
913 }
914 return $this->plainError( 'cite_error_references_no_text', $key );
915 }
916 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
917 }
918
929 private function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
930 $scope = strlen( $max );
931 $ret = MediaWikiServices::getInstance()->getContentLanguage()->formatNum(
932 sprintf( "%s.%0{$scope}s", $base, $offset )
933 );
934 return $ret;
935 }
936
948 if ( !isset( $this->mBacklinkLabels ) ) {
949 $this->genBacklinkLabels();
950 }
951 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
952 return $this->mBacklinkLabels[$offset];
953 } else {
954 // Feed me!
955 return $this->plainError( 'cite_error_references_no_backlink_label', null );
956 }
957 }
958
971 private function getLinkLabel( $offset, $group, $label ) {
972 $message = "cite_link_label_group-$group";
973 if ( !isset( $this->mLinkLabels[$group] ) ) {
974 $this->genLinkLabels( $group, $message );
975 }
976 if ( $this->mLinkLabels[$group] === false ) {
977 // Use normal representation, ie. "$group 1", "$group 2"...
978 return $label;
979 }
980
981 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
982 return $this->mLinkLabels[$group][$offset - 1];
983 } else {
984 // Feed me!
985 return $this->plainError( 'cite_error_no_link_label_group', [ $group, $message ] );
986 }
987 }
988
998 private function refKey( $key, $num = null ) {
999 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text();
1000 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text();
1001 if ( $num !== null ) {
1002 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num )
1003 ->inContentLanguage()->plain();
1004 }
1005
1006 return "$prefix$key$suffix";
1007 }
1008
1017 public static function getReferencesKey( $key ) {
1018 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text();
1019 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text();
1020
1021 return "$prefix$key$suffix";
1022 }
1023
1040 private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
1041 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1042
1043 if ( $label === null ) {
1044 $label = ++$this->mGroupCnt[$group];
1045 }
1046
1047 return $this->mParser->recursiveTagParse(
1048 wfMessage(
1049 'cite_reference_link',
1050 $this->normalizeKey(
1051 $this->refKey( $key, $count )
1052 ),
1053 $this->normalizeKey(
1054 self::getReferencesKey( $key . $subkey )
1055 ),
1056 Sanitizer::safeEncodeAttribute(
1057 $this->getLinkLabel( $label, $group,
1058 ( ( $group === self::DEFAULT_GROUP ) ? '' : "$group " ) . $contLang->formatNum( $label ) )
1059 )
1060 )->inContentLanguage()->plain()
1061 );
1062 }
1063
1070 private function normalizeKey( $key ) {
1071 $ret = Sanitizer::escapeIdForAttribute( $key );
1072 $ret = preg_replace( '/__+/', '_', $ret );
1073 $ret = Sanitizer::safeEncodeAttribute( $ret );
1074
1075 return $ret;
1076 }
1077
1088 private function listToText( $arr ) {
1089 $cnt = count( $arr );
1090 if ( $cnt === 1 ) {
1091 // Enforce always returning a string
1092 return (string)$arr[0];
1093 }
1094
1095 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain();
1096 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain();
1097 $t = array_slice( $arr, 0, $cnt - 1 );
1098 return implode( $sep, $t ) . $and . $arr[$cnt - 1];
1099 }
1100
1106 private function genBacklinkLabels() {
1107 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' )
1108 ->inContentLanguage()->plain();
1109 $this->mBacklinkLabels = preg_split( '/\s+/', $text );
1110 }
1111
1120 private function genLinkLabels( $group, $message ) {
1121 $text = false;
1122 $msg = wfMessage( $message )->inContentLanguage();
1123 if ( $msg->exists() ) {
1124 $text = $msg->plain();
1125 }
1126 $this->mLinkLabels[$group] = $text ? preg_split( '/\s+/', $text ) : false;
1127 }
1128
1135 public function clearState( Parser $parser ) {
1136 if ( $parser->extCite !== $this ) {
1137 $parser->extCite->clearState( $parser );
1138 return;
1139 }
1140
1141 # Don't clear state when we're in the middle of parsing
1142 # a <ref> tag
1143 if ( $this->mInCite || $this->mInReferences ) {
1144 return;
1145 }
1146
1147 $this->mGroupCnt = [];
1148 $this->mOutCnt = 0;
1149 $this->mCallCnt = 0;
1150 $this->mRefs = [];
1151 $this->mReferencesErrors = [];
1152 $this->mRefCallStack = [];
1153 }
1154
1160 public function cloneState( Parser $parser ) {
1161 if ( $parser->extCite !== $this ) {
1162 $parser->extCite->cloneState( $parser );
1163 return;
1164 }
1165
1166 $parser->extCite = clone $this;
1167 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1168 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1169
1170 // Clear the state, making sure it will actually work.
1171 $parser->extCite->mInCite = false;
1172 $parser->extCite->mInReferences = false;
1173 $parser->extCite->clearState( $parser );
1174 }
1175
1188 public function checkRefsNoReferences( $afterParse, $parser, &$text ) {
1189 global $wgCiteResponsiveReferences;
1190 if ( $parser->extCite === null ) {
1191 return;
1192 }
1193 if ( $parser->extCite !== $this ) {
1194 $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1195 return;
1196 }
1197
1198 if ( $afterParse ) {
1199 $this->mHaveAfterParse = true;
1200 } elseif ( $this->mHaveAfterParse ) {
1201 return;
1202 }
1203
1204 if ( !$parser->getOptions()->getIsPreview() ) {
1205 // save references data for later use by LinksUpdate hooks
1206 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1207 $this->saveReferencesData();
1208 }
1209 $isSectionPreview = false;
1210 } else {
1211 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1212 }
1213
1214 $s = '';
1215 foreach ( $this->mRefs as $group => $refs ) {
1216 if ( !$refs ) {
1217 continue;
1218 }
1219 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1220 $this->mInReferences = true;
1221 $s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
1222 $this->mInReferences = false;
1223 } else {
1224 $s .= "\n<br />" .
1225 $this->error(
1226 'cite_error_group_refs_without_references',
1227 Sanitizer::safeEncodeAttribute( $group )
1228 );
1229 }
1230 }
1231 if ( $isSectionPreview && $s !== '' ) {
1232 // provide a preview of references in its own section
1233 $text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
1234 $headerMsg = wfMessage( 'cite_section_preview_references' );
1235 if ( !$headerMsg->isDisabled() ) {
1236 $text .= '<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1237 . $headerMsg->escaped()
1238 . '</h2>';
1239 }
1240 $text .= $s . '</div>';
1241 } else {
1242 $text .= $s;
1243 }
1244 }
1245
1253 private function saveReferencesData( $group = self::DEFAULT_GROUP ) {
1254 global $wgCiteStoreReferencesData;
1255 if ( !$wgCiteStoreReferencesData ) {
1256 return;
1257 }
1258 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1259 if ( $savedRefs === null ) {
1260 // Initialize array structure
1261 $savedRefs = [
1262 'refs' => [],
1263 'version' => self::DATA_VERSION_NUMBER,
1264 ];
1265 }
1266 if ( $this->mBumpRefData ) {
1267 // This handles pages with multiple <references/> tags with <ref> tags in between.
1268 // On those, a group can appear several times, so we need to avoid overwriting
1269 // a previous appearance.
1270 $savedRefs['refs'][] = [];
1271 $this->mBumpRefData = false;
1272 }
1273 $n = count( $savedRefs['refs'] ) - 1;
1274 // save group
1275 $savedRefs['refs'][$n][$group] = $this->mRefs[$group];
1276
1277 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1278 }
1279
1285 public static function setHooks( Parser $parser ) {
1286 global $wgHooks;
1287
1288 $parser->extCite = new self();
1289
1290 if ( !self::$hooksInstalled ) {
1291 $wgHooks['ParserClearState'][] = [ $parser->extCite, 'clearState' ];
1292 $wgHooks['ParserCloned'][] = [ $parser->extCite, 'cloneState' ];
1293 $wgHooks['ParserAfterParse'][] = [ $parser->extCite, 'checkRefsNoReferences', true ];
1294 $wgHooks['ParserBeforeTidy'][] = [ $parser->extCite, 'checkRefsNoReferences', false ];
1295 self::$hooksInstalled = true;
1296 }
1297 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1298 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1299 }
1300
1308 private function error( $key, $param = null ) {
1309 $error = $this->plainError( $key, $param );
1310 return $this->mParser->recursiveTagParse( $error );
1311 }
1312
1321 private function plainError( $key, $param = null ) {
1322 # For ease of debugging and because errors are rare, we
1323 # use the user language and split the parser cache.
1324 $lang = $this->mParser->getOptions()->getUserLangObj();
1325 $dir = $lang->getDir();
1326
1327 # We rely on the fact that PHP is okay with passing unused argu-
1328 # ments to functions. If $1 is not used in the message, wfMessage will
1329 # just ignore the extra parameter.
1330 $msg = wfMessage(
1331 'cite_error',
1332 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1333 )
1334 ->inLanguage( $lang )
1335 ->plain();
1336
1337 $this->mParser->addTrackingCategory( 'cite-tracking-category-cite-error' );
1338
1339 $ret = Html::rawElement(
1340 'span',
1341 [
1342 'class' => 'error mw-ext-cite-error',
1343 'lang' => $lang->getHtmlCode(),
1344 'dir' => $dir,
1345 ],
1346 $msg
1347 );
1348
1349 return $ret;
1350 }
1351
1360 private function warning( $key, $param = null, $parse = 'parse' ) {
1361 # For ease of debugging and because errors are rare, we
1362 # use the user language and split the parser cache.
1363 $lang = $this->mParser->getOptions()->getUserLangObj();
1364 $dir = $lang->getDir();
1365
1366 # We rely on the fact that PHP is okay with passing unused argu-
1367 # ments to functions. If $1 is not used in the message, wfMessage will
1368 # just ignore the extra parameter.
1369 $msg = wfMessage(
1370 'cite_warning',
1371 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1372 )
1373 ->inLanguage( $lang )
1374 ->plain();
1375
1376 $key = preg_replace( '/^cite_warning_/', '', $key ) . '';
1377 $ret = Html::rawElement(
1378 'span',
1379 [
1380 'class' => 'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1381 Sanitizer::escapeClass( $key ),
1382 'lang' => $lang->getHtmlCode(),
1383 'dir' => $dir,
1384 ],
1385 $msg
1386 );
1387
1388 if ( $parse === 'parse' ) {
1389 $ret = $this->mParser->recursiveTagParse( $ret );
1390 }
1391
1392 return $ret;
1393 }
1394
1402 public static function getStoredReferences( Title $title ) {
1403 global $wgCiteStoreReferencesData;
1404 if ( !$wgCiteStoreReferencesData ) {
1405 return false;
1406 }
1407 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1408 $key = $cache->makeKey( self::EXT_DATA_KEY, $title->getArticleID() );
1409 return $cache->getWithSetCallback(
1410 $key,
1411 self::CACHE_DURATION_ONFETCH,
1412 function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
1413 $dbr = wfGetDB( DB_REPLICA );
1414 $setOpts += Database::getCacheSetOptions( $dbr );
1416 },
1417 [
1418 'checkKeys' => [ $key ],
1419 'lockTSE' => 30,
1420 ]
1421 );
1422 }
1423
1436 $string = '', $i = 1 ) {
1437 $id = $title->getArticleID();
1438 $result = $dbr->selectField(
1439 'page_props',
1440 'pp_value',
1441 [
1442 'pp_page' => $id,
1443 'pp_propname' => 'references-' . $i
1444 ],
1445 __METHOD__
1446 );
1447 if ( $result !== false ) {
1448 $string .= $result;
1449 $decodedString = gzdecode( $string );
1450 if ( $decodedString !== false ) {
1451 $json = json_decode( $decodedString, true );
1452 if ( json_last_error() === JSON_ERROR_NONE ) {
1453 return $json;
1454 }
1455 // corrupted json ?
1456 // shouldn't happen since when string is truncated, gzdecode should fail
1457 wfDebug( "Corrupted json detected when retrieving stored references for title id $id" );
1458 }
1459 // if gzdecode fails, try to fetch next references- property value
1460 return self::recursiveFetchRefsFromDB( $title, $dbr, $string, ++$i );
1461
1462 } else {
1463 // no refs stored in page_props at this index
1464 if ( $i > 1 ) {
1465 // shouldn't happen
1466 wfDebug( "Failed to retrieve stored references for title id $id" );
1467 }
1468 return false;
1469 }
1470 }
1471
1472}
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.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgHooks['AdminLinks'][]
Definition Cite.php:29
const CACHE_DURATION_ONFETCH
Cache duration set when fetching references from db.
Definition Cite.php:60
const DATA_VERSION_NUMBER
Version number in case we change the data structure in the future.
Definition Cite.php:50
getLinkLabel( $offset, $group, $label)
Generate a custom format link for a group given an offset, e.g.
Definition Cite.php:971
const EXT_DATA_KEY
Key used for storage in parser output's ExtensionData and ObjectCache.
Definition Cite.php:45
refArg(array $argv)
Parse the arguments to the <ref> tag.
Definition Cite.php:398
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:1040
referenceText( $key, $text)
Returns formatted reference text.
Definition Cite.php:909
const MAX_STORAGE_LENGTH
Maximum storage capacity for pp_value field of page_props table.
Definition Cite.php:40
genLinkLabels( $group, $message)
Generate the labels to pass to the 'cite_reference_link' message instead of numbers,...
Definition Cite.php:1120
genBacklinkLabels()
Generate the labels to pass to the 'cite_references_link_many_format' message, the format is an arbit...
Definition Cite.php:1106
stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser)
Populate $this->mRefs based on input and arguments to <ref>
Definition Cite.php:462
references( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <references>
Definition Cite.php:642
ref( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <ref>
Definition Cite.php:204
Parser $mParser
Definition Cite.php:134
int $mCallCnt
Counter to track the total number of (useful) calls to either the ref or references tag hook.
Definition Cite.php:113
string[] $mReferencesErrors
Error stack used when defining refs in <references>
Definition Cite.php:165
static Boolean $hooksInstalled
Did we install us into $wgHooks yet?
Definition Cite.php:192
saveReferencesData( $group=self::DEFAULT_GROUP)
Saves references in parser extension data This is called by each <references> tag,...
Definition Cite.php:1253
error( $key, $param=null)
Return an error message based on an error ID and parses it.
Definition Cite.php:1308
const DEFAULT_GROUP
Definition Cite.php:34
listToText( $arr)
This does approximately the same thing as Language::listToText() but due to this being used for a sli...
Definition Cite.php:1088
string[] $mBacklinkLabels
The backlinks, in order, to pass as $3 to 'cite_references_link_many_format', defined in 'cite_refere...
Definition Cite.php:122
static getStoredReferences(Title $title)
Fetch references stored for the given title in page_props For performance, results are cached.
Definition Cite.php:1402
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:998
const CACHE_DURATION_ONPARSE
Cache duration set when parsing a page with references.
Definition Cite.php:55
static recursiveFetchRefsFromDB(Title $title, IDatabase $dbr, $string='', $i=1)
Reconstructs compressed json by successively retrieving the properties references-1,...
Definition Cite.php:1435
boolean $mHaveAfterParse
True when the ParserAfterParse hook has been called.
Definition Cite.php:142
plainError( $key, $param=null)
Return an error message based on an error ID as unescaped plaintext.
Definition Cite.php:1321
cloneState(Parser $parser)
Gets run when the parser is cloned.
Definition Cite.php:1160
referencesFormatEntryAlternateBacklinkLabel( $offset)
Generate a custom format backlink given an offset, e.g.
Definition Cite.php:947
referencesFormat( $group, $responsive)
Make output to be returned from the references() function.
Definition Cite.php:761
string[] false[] $mLinkLabels
The links to use per group, in order.
Definition Cite.php:129
array false[] $mRefCallStack
<ref> call stack Used to cleanup out of sequence ref calls created by #tag See description of functio...
Definition Cite.php:181
int $mOutCnt
Count for user displayed output (ref[1], ref[2], ...)
Definition Cite.php:100
int[] $mGroupCnt
Definition Cite.php:105
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:1188
bool $mBumpRefData
Definition Cite.php:186
referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max)
Generate a numeric backlink given a base number and an offset, e.g.
Definition Cite.php:929
normalizeKey( $key)
Normalizes and sanitizes a reference key.
Definition Cite.php:1070
guardedRef( $str, array $argv, Parser $parser)
Definition Cite.php:237
static setHooks(Parser $parser)
Initialize the parser hooks.
Definition Cite.php:1285
boolean $mInCite
True when a <ref> tag is being processed.
Definition Cite.php:150
static getReferencesKey( $key)
Return an id for use in wikitext output based on a key and optionally the number of it,...
Definition Cite.php:1017
boolean $mInReferences
True when a <references> tag is being processed.
Definition Cite.php:158
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:1135
string $mReferencesGroup
Group used when in <references> block.
Definition Cite.php:172
referencesFormatEntry( $key, $val)
Format a single entry for the referencesFormat() function.
Definition Cite.php:810
guardedReferences( $str, array $argv, Parser $parser)
Must only be called from references().
Definition Cite.php:666
array[] $mRefs
Datastructure representing <ref> input, in the format of: [ 'user supplied' => [ 'text' => 'user sup...
Definition Cite.php:93
rollbackRef( $type, $key, $group, $index)
Partially undoes the effect of calls to stack()
Definition Cite.php:587
warning( $key, $param=null, $parse='parse')
Return a warning message based on a warning ID.
Definition Cite.php:1360
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:74
getOptions()
Get the ParserOptions object.
Definition Parser.php:992
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:5189
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:795
getOutput()
Get the ParserOutput object.
Definition Parser.php:983
clearState()
Clear Parser state.
Definition Parser.php:484
Represents a title within MediaWiki.
Definition Title.php:42
Relational database abstraction object.
Definition Database.php:49
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
return true
Definition router.php:94
if(!isset( $args[0])) $lang