MediaWiki REL1_32
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 $frame->setVolatile();
225
226 // new <ref> tag, we may need to bump the ref data counter
227 // to avoid overwriting a previous group
228 $this->mBumpRefData = true;
229
230 return $ret;
231 }
232
242 private function guardedRef(
243 $str,
244 array $argv,
246 $default_group = self::DEFAULT_GROUP
247 ) {
248 $this->mParser = $parser;
249
250 # The key here is the "name" attribute.
251 list( $key, $group, $follow, $dir ) = $this->refArg( $argv );
252 // empty string indicate invalid dir
253 if ( $dir === '' && $str !== '' ) {
254 $str .= $this->plainError( 'cite_error_ref_invalid_dir', $argv['dir'] );
255 }
256 # Split these into groups.
257 if ( $group === null ) {
258 if ( $this->mInReferences ) {
260 } else {
261 $group = $default_group;
262 }
263 }
264
265 /*
266 * This section deals with constructions of the form
267 *
268 * <references>
269 * <ref name="foo"> BAR </ref>
270 * </references>
271 */
272 if ( $this->mInReferences ) {
273 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
274 if ( $group != $this->mReferencesGroup ) {
275 # <ref> and <references> have conflicting group attributes.
276 $this->mReferencesErrors[] =
277 $this->error(
278 'cite_error_references_group_mismatch',
279 Sanitizer::safeEncodeAttribute( $group )
280 );
281 } elseif ( $str !== '' ) {
282 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
283 # Called with group attribute not defined in text.
284 $this->mReferencesErrors[] =
285 $this->error(
286 'cite_error_references_missing_group',
287 Sanitizer::safeEncodeAttribute( $group )
288 );
289 } elseif ( $key === null || $key === '' ) {
290 # <ref> calls inside <references> must be named
291 $this->mReferencesErrors[] =
292 $this->error( 'cite_error_references_no_key' );
293 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
294 # Called with name attribute not defined in text.
295 $this->mReferencesErrors[] =
296 $this->error( 'cite_error_references_missing_key', Sanitizer::safeEncodeAttribute( $key ) );
297 } else {
298 if (
299 isset( $this->mRefs[$group][$key]['text'] ) &&
300 $str !== $this->mRefs[$group][$key]['text']
301 ) {
302 // two refs with same key and different content
303 // add error message to the original ref
304 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
305 'cite_error_references_duplicate_key', $key
306 );
307 } else {
308 # Assign the text to corresponding ref
309 $this->mRefs[$group][$key]['text'] = $str;
310 }
311 }
312 } else {
313 # <ref> called in <references> has no content.
314 $this->mReferencesErrors[] =
315 $this->error( 'cite_error_empty_references_define', Sanitizer::safeEncodeAttribute( $key ) );
316 }
317 return '';
318 }
319
320 if ( $str === '' ) {
321 # <ref ...></ref>. This construct is invalid if
322 # it's a contentful ref, but OK if it's a named duplicate and should
323 # be equivalent <ref ... />, for compatability with #tag.
324 if ( is_string( $key ) && $key !== '' ) {
325 $str = null;
326 } else {
327 $this->mRefCallStack[] = false;
328
329 return $this->error( 'cite_error_ref_no_input' );
330 }
331 }
332
333 if ( $key === false ) {
334 # TODO: Comment this case; what does this condition mean?
335 $this->mRefCallStack[] = false;
336 return $this->error( 'cite_error_ref_too_many_keys' );
337 }
338
339 if ( $str === null && $key === null ) {
340 # Something like <ref />; this makes no sense.
341 $this->mRefCallStack[] = false;
342 return $this->error( 'cite_error_ref_no_key' );
343 }
344
345 if ( is_string( $key ) && preg_match( '/^[0-9]+$/', $key ) ||
346 is_string( $follow ) && preg_match( '/^[0-9]+$/', $follow )
347 ) {
348 # Numeric names mess up the resulting id's, potentially produ-
349 # cing duplicate id's in the XHTML. The Right Thing To Do
350 # would be to mangle them, but it's not really high-priority
351 # (and would produce weird id's anyway).
352
353 $this->mRefCallStack[] = false;
354 return $this->error( 'cite_error_ref_numeric_key' );
355 }
356
357 if ( preg_match(
358 '/<ref\b[^<]*?>/',
359 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
360 ) ) {
361 # (bug T8199) This most likely implies that someone left off the
362 # closing </ref> tag, which will cause the entire article to be
363 # eaten up until the next <ref>. So we bail out early instead.
364 # The fancy regex above first tries chopping out anything that
365 # looks like a comment or SGML tag, which is a crude way to avoid
366 # false alarms for <nowiki>, <pre>, etc.
367
368 # Possible improvement: print the warning, followed by the contents
369 # of the <ref> tag. This way no part of the article will be eaten
370 # even temporarily.
371
372 $this->mRefCallStack[] = false;
373 return $this->error( 'cite_error_included_ref' );
374 }
375
376 if ( is_string( $key ) || is_string( $str ) ) {
377 # We don't care about the content: if the key exists, the ref
378 # is presumptively valid. Either it stores a new ref, or re-
379 # fers to an existing one. If it refers to a nonexistent ref,
380 # we'll figure that out later. Likewise it's definitely valid
381 # if there's any content, regardless of key.
382
383 return $this->stack( $str, $key, $group, $follow, $argv, $dir, $parser );
384 }
385
386 # Not clear how we could get here, but something is probably
387 # wrong with the types. Let's fail fast.
388 throw new Exception( 'Invalid $str and/or $key: ' . serialize( [ $str, $key ] ) );
389 }
390
404 private function refArg( array $argv ) {
405 $cnt = count( $argv );
406 $group = null;
407 $key = null;
408 $follow = null;
409 $dir = null;
410 if ( isset( $argv['dir'] ) ) {
411 // compare the dir attribute value against an explicit whitelist.
412 $dir = '';
413 $isValidDir = in_array( strtolower( $argv['dir'] ), [ 'ltr', 'rtl' ] );
414 if ( $isValidDir ) {
415 $dir = Html::expandAttributes( [ 'class' => 'mw-cite-dir-' . strtolower( $argv['dir'] ) ] );
416 }
417
418 unset( $argv['dir'] );
419 --$cnt;
420 }
421 if ( $cnt > 2 ) {
422 // There should only be one key or follow parameter, and one group parameter
423 // FIXME : this looks inconsistent, it should probably return a tuple
424 return false;
425 } elseif ( $cnt >= 1 ) {
426 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
427 return [ false, false, false, false ];
428 }
429 if ( isset( $argv['name'] ) ) {
430 // Key given.
431 $key = trim( $argv['name'] );
432 unset( $argv['name'] );
433 --$cnt;
434 }
435 if ( isset( $argv['follow'] ) ) {
436 // Follow given.
437 $follow = trim( $argv['follow'] );
438 unset( $argv['follow'] );
439 --$cnt;
440 }
441 if ( isset( $argv['group'] ) ) {
442 // Group given.
443 $group = $argv['group'];
444 unset( $argv['group'] );
445 --$cnt;
446 }
447
448 if ( $cnt === 0 ) {
449 return [ $key, $group, $follow, $dir ];
450 } else {
451 // Invalid key
452 return [ false, false, false, false ];
453 }
454 } else {
455 // No key
456 return [ null, $group, false, $dir ];
457 }
458 }
459
474 private function stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser ) {
475 if ( !isset( $this->mRefs[$group] ) ) {
476 $this->mRefs[$group] = [];
477 }
478 if ( !isset( $this->mGroupCnt[$group] ) ) {
479 $this->mGroupCnt[$group] = 0;
480 }
481 if ( $follow != null ) {
482 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
483 // add text to the note that is being followed
484 $this->mRefs[$group][$follow]['text'] .= ' ' . $str;
485 } else {
486 // insert part of note at the beginning of the group
487 $groupsCount = count( $this->mRefs[$group] );
488 for ( $k = 0; $k < $groupsCount; $k++ ) {
489 if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
490 break;
491 }
492 }
493 array_splice( $this->mRefs[$group], $k, 0, [ [
494 'count' => -1,
495 'text' => $str,
496 'key' => ++$this->mOutCnt,
497 'follow' => $follow,
498 'dir' => $dir
499 ] ] );
500 array_splice( $this->mRefCallStack, $k, 0,
501 [ [ 'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
502 }
503 // return an empty string : this is not a reference
504 return '';
505 }
506
507 if ( $key === null ) {
508 // No key
509 // $this->mRefs[$group][] = $str;
510
511 $this->mRefs[$group][] = [
512 'count' => -1,
513 'text' => $str,
514 'key' => ++$this->mOutCnt,
515 'dir' => $dir
516 ];
517 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
518
519 return $this->linkRef( $group, $this->mOutCnt );
520 }
521 if ( !is_string( $key ) ) {
522 throw new Exception( 'Invalid stack key: ' . serialize( $key ) );
523 }
524
525 // Valid key
526 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
527 // First occurrence
528 $this->mRefs[$group][$key] = [
529 'text' => $str,
530 'count' => 0,
531 'key' => ++$this->mOutCnt,
532 'number' => ++$this->mGroupCnt[$group],
533 'dir' => $dir
534 ];
535 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
536
537 return $this->linkRef(
538 $group,
539 $key,
540 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
541 $this->mRefs[$group][$key]['number'],
542 "-" . $this->mRefs[$group][$key]['key']
543 );
544 }
545
546 // We've been here before
547 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
548 // If no text found before, use this text
549 $this->mRefs[$group][$key]['text'] = $str;
550 // Use the dir parameter only from the full definition of a named ref tag
551 $this->mRefs[$group][$key]['dir'] = $dir;
552 $this->mRefCallStack[] = [ 'assign', $call, $str, $key, $group,
553 $this->mRefs[$group][$key]['key'] ];
554 } else {
555 if ( $str != null && $str !== ''
556 // T205803 different strip markers might hide the same text
557 && $parser->mStripState->unstripBoth( $str )
558 !== $parser->mStripState->unstripBoth( $this->mRefs[$group][$key]['text'] )
559 ) {
560 // two refs with same key and different content
561 // add error message to the original ref
562 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
563 'cite_error_references_duplicate_key', $key
564 );
565 }
566 $this->mRefCallStack[] = [ 'increment', $call, $str, $key, $group,
567 $this->mRefs[$group][$key]['key'] ];
568 }
569 return $this->linkRef(
570 $group,
571 $key,
572 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
573 $this->mRefs[$group][$key]['number'],
574 "-" . $this->mRefs[$group][$key]['key']
575 );
576 }
577
599 private function rollbackRef( $type, $key, $group, $index ) {
600 if ( !isset( $this->mRefs[$group] ) ) {
601 return;
602 }
603
604 if ( $key === null ) {
605 foreach ( $this->mRefs[$group] as $k => $v ) {
606 if ( $this->mRefs[$group][$k]['key'] === $index ) {
607 $key = $k;
608 break;
609 }
610 }
611 }
612
613 // Sanity checks that specified element exists.
614 if ( $key === null ) {
615 return;
616 }
617 if ( !isset( $this->mRefs[$group][$key] ) ) {
618 return;
619 }
620 if ( $this->mRefs[$group][$key]['key'] != $index ) {
621 return;
622 }
623
624 switch ( $type ) {
625 case 'new':
626 # Rollback the addition of new elements to the stack.
627 unset( $this->mRefs[$group][$key] );
628 if ( $this->mRefs[$group] === [] ) {
629 unset( $this->mRefs[$group] );
630 unset( $this->mGroupCnt[$group] );
631 }
632 break;
633 case 'assign':
634 # Rollback assignment of text to pre-existing elements.
635 $this->mRefs[$group][$key]['text'] = null;
636 # continue without break
637 case 'increment':
638 # Rollback increase in named ref occurrences.
639 $this->mRefs[$group][$key]['count']--;
640 break;
641 }
642 }
643
654 public function references( $str, array $argv, Parser $parser, PPFrame $frame ) {
655 if ( $this->mInCite || $this->mInReferences ) {
656 if ( is_null( $str ) ) {
657 return htmlspecialchars( "<references/>" );
658 }
659 return htmlspecialchars( "<references>$str</references>" );
660 }
661 $this->mCallCnt++;
662 $this->mInReferences = true;
663 $ret = $this->guardedReferences( $str, $argv, $parser );
664 $this->mInReferences = false;
665 $frame->setVolatile();
666 return $ret;
667 }
668
678 private function guardedReferences(
679 $str,
680 array $argv,
682 $group = self::DEFAULT_GROUP
683 ) {
684 global $wgCiteResponsiveReferences;
685
686 $this->mParser = $parser;
687
688 if ( isset( $argv['group'] ) ) {
689 $group = $argv['group'];
690 unset( $argv['group'] );
691 }
692
693 if ( strval( $str ) !== '' ) {
694 $this->mReferencesGroup = $group;
695
696 # Detect whether we were sent already rendered <ref>s.
697 # Mostly a side effect of using #tag to call references.
698 # The following assumes that the parsed <ref>s sent within
699 # the <references> block were the most recent calls to
700 # <ref>. This assumption is true for all known use cases,
701 # but not strictly enforced by the parser. It is possible
702 # that some unusual combination of #tag, <references> and
703 # conditional parser functions could be created that would
704 # lead to malformed references here.
705 $count = substr_count( $str, Parser::MARKER_PREFIX . "-ref-" );
706 $redoStack = [];
707
708 # Undo effects of calling <ref> while unaware of containing <references>
709 for ( $i = 1; $i <= $count; $i++ ) {
710 if ( !$this->mRefCallStack ) {
711 break;
712 }
713
714 $call = array_pop( $this->mRefCallStack );
715 $redoStack[] = $call;
716 if ( $call !== false ) {
717 list( $type, $ref_argv, $ref_str,
718 $ref_key, $ref_group, $ref_index ) = $call;
719 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
720 }
721 }
722
723 # Rerun <ref> call now that mInReferences is set.
724 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
725 $call = $redoStack[$i];
726 if ( $call !== false ) {
727 list( $type, $ref_argv, $ref_str,
728 $ref_key, $ref_group, $ref_index ) = $call;
729 $this->guardedRef( $ref_str, $ref_argv, $parser );
730 }
731 }
732
733 # Parse $str to process any unparsed <ref> tags.
734 $parser->recursiveTagParse( $str );
735
736 # Reset call stack
737 $this->mRefCallStack = [];
738 }
739
740 if ( isset( $argv['responsive'] ) ) {
741 $responsive = $argv['responsive'] !== '0';
742 unset( $argv['responsive'] );
743 } else {
744 $responsive = $wgCiteResponsiveReferences;
745 }
746
747 // There are remaining parameters we don't recognise
748 if ( $argv ) {
749 return $this->error( 'cite_error_references_invalid_parameters' );
750 }
751
752 $s = $this->referencesFormat( $group, $responsive );
753
754 # Append errors generated while processing <references>
755 if ( $this->mReferencesErrors ) {
756 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
757 $this->mReferencesErrors = [];
758 }
759 return $s;
760 }
761
772 private function referencesFormat( $group, $responsive ) {
773 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
774 return '';
775 }
776
777 $ent = [];
778 foreach ( $this->mRefs[$group] as $k => $v ) {
779 $ent[] = $this->referencesFormatEntry( $k, $v );
780 }
781
782 // Add new lines between the list items (ref entires) to avoid confusing tidy (T15073).
783 // Note: This builds a string of wikitext, not html.
784 $parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ],
785 "\n" . implode( "\n", $ent ) . "\n"
786 );
787
788 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
789 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" );
790
791 if ( $responsive ) {
792 // Use a DIV wrap because column-count on a list directly is broken in Chrome.
793 // See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
794 $wrapClasses = [ 'mw-references-wrap' ];
795 if ( count( $this->mRefs[$group] ) > 10 ) {
796 $wrapClasses[] = 'mw-references-columns';
797 }
798 $ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
799 }
800
801 if ( !$this->mParser->getOptions()->getIsPreview() ) {
802 // save references data for later use by LinksUpdate hooks
803 $this->saveReferencesData( $group );
804 }
805
806 // done, clean up so we can reuse the group
807 unset( $this->mRefs[$group] );
808 unset( $this->mGroupCnt[$group] );
809
810 return $ret;
811 }
812
821 private function referencesFormatEntry( $key, $val ) {
822 // Anonymous reference
823 if ( !is_array( $val ) ) {
824 return wfMessage(
825 'cite_references_link_one',
826 $this->normalizeKey(
827 self::getReferencesKey( $key )
828 ),
829 $this->normalizeKey(
830 $this->refKey( $key )
831 ),
832 $this->referenceText( $key, $val ),
833 $val['dir']
834 )->inContentLanguage()->plain();
835 }
836 $text = $this->referenceText( $key, $val['text'] );
837 if ( isset( $val['follow'] ) ) {
838 return wfMessage(
839 'cite_references_no_link',
840 $this->normalizeKey(
841 self::getReferencesKey( $val['follow'] )
842 ),
843 $text
844 )->inContentLanguage()->plain();
845 }
846 if ( !isset( $val['count'] ) ) {
847 // this handles the case of section preview for list-defined references
848 return wfMessage( 'cite_references_link_many',
849 $this->normalizeKey(
850 self::getReferencesKey( $key . "-" . ( isset( $val['key'] ) ? $val['key'] : '' ) )
851 ),
852 '',
853 $text
854 )->inContentLanguage()->plain();
855 }
856 if ( $val['count'] < 0 ) {
857 return wfMessage(
858 'cite_references_link_one',
859 $this->normalizeKey(
860 self::getReferencesKey( $val['key'] )
861 ),
862 $this->normalizeKey(
863 # $this->refKey( $val['key'], $val['count'] )
864 $this->refKey( $val['key'] )
865 ),
866 $text,
867 $val['dir']
868 )->inContentLanguage()->plain();
869 // Standalone named reference, I want to format this like an
870 // anonymous reference because displaying "1. 1.1 Ref text" is
871 // overkill and users frequently use named references when they
872 // don't need them for convenience
873 }
874 if ( $val['count'] === 0 ) {
875 return wfMessage(
876 'cite_references_link_one',
877 $this->normalizeKey(
878 self::getReferencesKey( $key . "-" . $val['key'] )
879 ),
880 $this->normalizeKey(
881 # $this->refKey( $key, $val['count'] ),
882 $this->refKey( $key, $val['key'] . "-" . $val['count'] )
883 ),
884 $text,
885 $val['dir']
886 )->inContentLanguage()->plain();
887 // Named references with >1 occurrences
888 }
889 $links = [];
890 // for group handling, we have an extra key here.
891 for ( $i = 0; $i <= $val['count']; ++$i ) {
892 $links[] = wfMessage(
893 'cite_references_link_many_format',
894 $this->normalizeKey(
895 $this->refKey( $key, $val['key'] . "-$i" )
896 ),
897 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
899 )->inContentLanguage()->plain();
900 }
901
902 $list = $this->listToText( $links );
903
904 return wfMessage( 'cite_references_link_many',
905 $this->normalizeKey(
906 self::getReferencesKey( $key . "-" . $val['key'] )
907 ),
908 $list,
909 $text,
910 $val['dir']
911 )->inContentLanguage()->plain();
912 }
913
920 private function referenceText( $key, $text ) {
921 if ( !isset( $text ) || $text === '' ) {
922 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
923 return $this->warning( 'cite_warning_sectionpreview_no_text', $key, 'noparse' );
924 }
925 return $this->plainError( 'cite_error_references_no_text', $key );
926 }
927 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
928 }
929
940 private function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
941 global $wgContLang;
942 $scope = strlen( $max );
943 $ret = $wgContLang->formatNum(
944 sprintf( "%s.%0{$scope}s", $base, $offset )
945 );
946 return $ret;
947 }
948
960 if ( !isset( $this->mBacklinkLabels ) ) {
961 $this->genBacklinkLabels();
962 }
963 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
964 return $this->mBacklinkLabels[$offset];
965 } else {
966 // Feed me!
967 return $this->plainError( 'cite_error_references_no_backlink_label', null );
968 }
969 }
970
983 private function getLinkLabel( $offset, $group, $label ) {
984 $message = "cite_link_label_group-$group";
985 if ( !isset( $this->mLinkLabels[$group] ) ) {
986 $this->genLinkLabels( $group, $message );
987 }
988 if ( $this->mLinkLabels[$group] === false ) {
989 // Use normal representation, ie. "$group 1", "$group 2"...
990 return $label;
991 }
992
993 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
994 return $this->mLinkLabels[$group][$offset - 1];
995 } else {
996 // Feed me!
997 return $this->plainError( 'cite_error_no_link_label_group', [ $group, $message ] );
998 }
999 }
1000
1010 private function refKey( $key, $num = null ) {
1011 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text();
1012 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text();
1013 if ( isset( $num ) ) {
1014 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num )
1015 ->inContentLanguage()->plain();
1016 }
1017
1018 return "$prefix$key$suffix";
1019 }
1020
1029 public static function getReferencesKey( $key ) {
1030 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text();
1031 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text();
1032
1033 return "$prefix$key$suffix";
1034 }
1035
1052 private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
1053 global $wgContLang;
1054 $label = is_null( $label ) ? ++$this->mGroupCnt[$group] : $label;
1055
1056 return $this->mParser->recursiveTagParse(
1057 wfMessage(
1058 'cite_reference_link',
1059 $this->normalizeKey(
1060 $this->refKey( $key, $count )
1061 ),
1062 $this->normalizeKey(
1063 self::getReferencesKey( $key . $subkey )
1064 ),
1065 Sanitizer::safeEncodeAttribute(
1066 $this->getLinkLabel( $label, $group,
1067 ( ( $group === self::DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) )
1068 )
1069 )->inContentLanguage()->plain()
1070 );
1071 }
1072
1079 private function normalizeKey( $key ) {
1080 $key = Sanitizer::escapeIdForAttribute( $key );
1081 $key = Sanitizer::safeEncodeAttribute( $key );
1082
1083 return $key;
1084 }
1085
1096 private function listToText( $arr ) {
1097 $cnt = count( $arr );
1098
1099 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain();
1100 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain();
1101
1102 if ( $cnt === 1 ) {
1103 // Enforce always returning a string
1104 return (string)$arr[0];
1105 } else {
1106 $t = array_slice( $arr, 0, $cnt - 1 );
1107 return implode( $sep, $t ) . $and . $arr[$cnt - 1];
1108 }
1109 }
1110
1116 private function genBacklinkLabels() {
1117 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' )
1118 ->inContentLanguage()->plain();
1119 $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text );
1120 }
1121
1130 private function genLinkLabels( $group, $message ) {
1131 $text = false;
1132 $msg = wfMessage( $message )->inContentLanguage();
1133 if ( $msg->exists() ) {
1134 $text = $msg->plain();
1135 }
1136 $this->mLinkLabels[$group] = ( !$text ) ? false : preg_split( '#[\n\t ]#', $text );
1137 }
1138
1145 public function clearState( Parser $parser ) {
1146 if ( $parser->extCite !== $this ) {
1147 $parser->extCite->clearState( $parser );
1148 return;
1149 }
1150
1151 # Don't clear state when we're in the middle of parsing
1152 # a <ref> tag
1153 if ( $this->mInCite || $this->mInReferences ) {
1154 return;
1155 }
1156
1157 $this->mGroupCnt = [];
1158 $this->mOutCnt = 0;
1159 $this->mCallCnt = 0;
1160 $this->mRefs = [];
1161 $this->mReferencesErrors = [];
1162 $this->mRefCallStack = [];
1163 }
1164
1170 public function cloneState( Parser $parser ) {
1171 if ( $parser->extCite !== $this ) {
1172 $parser->extCite->cloneState( $parser );
1173 return;
1174 }
1175
1176 $parser->extCite = clone $this;
1177 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1178 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1179
1180 // Clear the state, making sure it will actually work.
1181 $parser->extCite->mInCite = false;
1182 $parser->extCite->mInReferences = false;
1183 $parser->extCite->clearState( $parser );
1184 }
1185
1198 public function checkRefsNoReferences( $afterParse, $parser, &$text ) {
1199 global $wgCiteResponsiveReferences;
1200 if ( is_null( $parser->extCite ) ) {
1201 return;
1202 }
1203 if ( $parser->extCite !== $this ) {
1204 $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1205 return;
1206 }
1207
1208 if ( $afterParse ) {
1209 $this->mHaveAfterParse = true;
1210 } elseif ( $this->mHaveAfterParse ) {
1211 return;
1212 }
1213
1214 if ( !$parser->getOptions()->getIsPreview() ) {
1215 // save references data for later use by LinksUpdate hooks
1216 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1217 $this->saveReferencesData();
1218 }
1219 $isSectionPreview = false;
1220 } else {
1221 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1222 }
1223
1224 $s = '';
1225 foreach ( $this->mRefs as $group => $refs ) {
1226 if ( !$refs ) {
1227 continue;
1228 }
1229 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1230 $this->mInReferences = true;
1231 $s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
1232 $this->mInReferences = false;
1233 } else {
1234 $s .= "\n<br />" .
1235 $this->error(
1236 'cite_error_group_refs_without_references',
1237 Sanitizer::safeEncodeAttribute( $group )
1238 );
1239 }
1240 }
1241 if ( $isSectionPreview && $s !== '' ) {
1242 // provide a preview of references in its own section
1243 $text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
1244 $headerMsg = wfMessage( 'cite_section_preview_references' );
1245 if ( !$headerMsg->isDisabled() ) {
1246 $text .= '<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1247 . $headerMsg->escaped()
1248 . '</h2>';
1249 }
1250 $text .= $s . '</div>';
1251 } else {
1252 $text .= $s;
1253 }
1254 }
1255
1263 private function saveReferencesData( $group = self::DEFAULT_GROUP ) {
1264 global $wgCiteStoreReferencesData;
1265 if ( !$wgCiteStoreReferencesData ) {
1266 return;
1267 }
1268 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1269 if ( $savedRefs === null ) {
1270 // Initialize array structure
1271 $savedRefs = [
1272 'refs' => [],
1273 'version' => self::DATA_VERSION_NUMBER,
1274 ];
1275 }
1276 if ( $this->mBumpRefData ) {
1277 // This handles pages with multiple <references/> tags with <ref> tags in between.
1278 // On those, a group can appear several times, so we need to avoid overwriting
1279 // a previous appearance.
1280 $savedRefs['refs'][] = [];
1281 $this->mBumpRefData = false;
1282 }
1283 $n = count( $savedRefs['refs'] ) - 1;
1284 // save group
1285 $savedRefs['refs'][$n][$group] = $this->mRefs[$group];
1286
1287 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1288 }
1289
1295 public static function setHooks( Parser $parser ) {
1296 global $wgHooks;
1297
1298 $parser->extCite = new self();
1299
1300 if ( !self::$hooksInstalled ) {
1301 $wgHooks['ParserClearState'][] = [ $parser->extCite, 'clearState' ];
1302 $wgHooks['ParserCloned'][] = [ $parser->extCite, 'cloneState' ];
1303 $wgHooks['ParserAfterParse'][] = [ $parser->extCite, 'checkRefsNoReferences', true ];
1304 $wgHooks['ParserBeforeTidy'][] = [ $parser->extCite, 'checkRefsNoReferences', false ];
1305 self::$hooksInstalled = true;
1306 }
1307 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1308 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1309 }
1310
1318 private function error( $key, $param = null ) {
1319 $error = $this->plainError( $key, $param );
1320 return $this->mParser->recursiveTagParse( $error );
1321 }
1322
1331 private function plainError( $key, $param = null ) {
1332 # For ease of debugging and because errors are rare, we
1333 # use the user language and split the parser cache.
1334 $lang = $this->mParser->getOptions()->getUserLangObj();
1335 $dir = $lang->getDir();
1336
1337 # We rely on the fact that PHP is okay with passing unused argu-
1338 # ments to functions. If $1 is not used in the message, wfMessage will
1339 # just ignore the extra parameter.
1340 $msg = wfMessage(
1341 'cite_error',
1342 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1343 )
1344 ->inLanguage( $lang )
1345 ->plain();
1346
1347 $this->mParser->addTrackingCategory( 'cite-tracking-category-cite-error' );
1348
1349 $ret = Html::rawElement(
1350 'span',
1351 [
1352 'class' => 'error mw-ext-cite-error',
1353 'lang' => $lang->getHtmlCode(),
1354 'dir' => $dir,
1355 ],
1356 $msg
1357 );
1358
1359 return $ret;
1360 }
1361
1370 private function warning( $key, $param = null, $parse = 'parse' ) {
1371 # For ease of debugging and because errors are rare, we
1372 # use the user language and split the parser cache.
1373 $lang = $this->mParser->getOptions()->getUserLangObj();
1374 $dir = $lang->getDir();
1375
1376 # We rely on the fact that PHP is okay with passing unused argu-
1377 # ments to functions. If $1 is not used in the message, wfMessage will
1378 # just ignore the extra parameter.
1379 $msg = wfMessage(
1380 'cite_warning',
1381 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1382 )
1383 ->inLanguage( $lang )
1384 ->plain();
1385
1386 $key = preg_replace( '/^cite_warning_/', '', $key ) . '';
1387 $ret = Html::rawElement(
1388 'span',
1389 [
1390 'class' => 'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1391 Sanitizer::escapeClass( $key ),
1392 'lang' => $lang->getHtmlCode(),
1393 'dir' => $dir,
1394 ],
1395 $msg
1396 );
1397
1398 if ( $parse === 'parse' ) {
1399 $ret = $this->mParser->recursiveTagParse( $ret );
1400 }
1401
1402 return $ret;
1403 }
1404
1412 public static function getStoredReferences( Title $title ) {
1413 global $wgCiteStoreReferencesData;
1414 if ( !$wgCiteStoreReferencesData ) {
1415 return false;
1416 }
1417 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1418 $key = $cache->makeKey( self::EXT_DATA_KEY, $title->getArticleID() );
1419 return $cache->getWithSetCallback(
1420 $key,
1421 self::CACHE_DURATION_ONFETCH,
1422 function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
1423 $dbr = wfGetDB( DB_REPLICA );
1424 $setOpts += Database::getCacheSetOptions( $dbr );
1425 return self::recursiveFetchRefsFromDB( $title, $dbr );
1426 },
1427 [
1428 'checkKeys' => [ $key ],
1429 'lockTSE' => 30,
1430 ]
1431 );
1432 }
1433
1445 private static function recursiveFetchRefsFromDB( Title $title, IDatabase $dbr,
1446 $string = '', $i = 1 ) {
1447 $id = $title->getArticleID();
1448 $result = $dbr->selectField(
1449 'page_props',
1450 'pp_value',
1451 [
1452 'pp_page' => $id,
1453 'pp_propname' => 'references-' . $i
1454 ],
1455 __METHOD__
1456 );
1457 if ( $result !== false ) {
1458 $string .= $result;
1459 $decodedString = gzdecode( $string );
1460 if ( $decodedString !== false ) {
1461 $json = json_decode( $decodedString, true );
1462 if ( json_last_error() === JSON_ERROR_NONE ) {
1463 return $json;
1464 }
1465 // corrupted json ?
1466 // shouldn't happen since when string is truncated, gzdecode should fail
1467 wfDebug( "Corrupted json detected when retrieving stored references for title id $id" );
1468 }
1469 // if gzdecode fails, try to fetch next references- property value
1470 return self::recursiveFetchRefsFromDB( $title, $dbr, $string, ++$i );
1471
1472 } else {
1473 // no refs stored in page_props at this index
1474 if ( $i > 1 ) {
1475 // shouldn't happen
1476 wfDebug( "Failed to retrieve stored references for title id $id" );
1477 }
1478 return false;
1479 }
1480 }
1481
1482}
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.
$wgContLang
Definition Setup.php:809
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:983
guardedReferences( $str, array $argv, Parser $parser, $group=self::DEFAULT_GROUP)
Must only be called from references().
Definition Cite.php:678
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:404
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:1052
referenceText( $key, $text)
Returns formatted reference text.
Definition Cite.php:920
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:1130
genBacklinkLabels()
Generate the labels to pass to the 'cite_references_link_many_format' message, the format is an arbit...
Definition Cite.php:1116
stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser)
Populate $this->mRefs based on input and arguments to <ref>
Definition Cite.php:474
references( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <references>
Definition Cite.php:654
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
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:1263
guardedRef( $str, array $argv, Parser $parser, $default_group=self::DEFAULT_GROUP)
Definition Cite.php:242
error( $key, $param=null)
Return an error message based on an error ID and parses it.
Definition Cite.php:1318
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:1096
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:1412
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:1010
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:1445
boolean $mHaveAfterParse
True when the ParserAfterParse hook has been called.
Definition Cite.php:146
plainError( $key, $param=null)
Return an error message based on an error ID as unescaped plaintext.
Definition Cite.php:1331
cloneState(Parser $parser)
Gets run when the parser is cloned.
Definition Cite.php:1170
referencesFormatEntryAlternateBacklinkLabel( $offset)
Generate a custom format backlink given an offset, e.g.
Definition Cite.php:959
referencesFormat( $group, $responsive)
Make output to be returned from the references() function.
Definition Cite.php:772
int $mOutCnt
Count for user displayed output (ref[1], ref[2], ...)
Definition Cite.php:104
int[] $mGroupCnt
Definition Cite.php:109
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:1198
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:940
normalizeKey( $key)
Normalizes and sanitizes a reference key.
Definition Cite.php:1079
static setHooks(Parser $parser)
Initialize the parser hooks.
Definition Cite.php:1295
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:1029
boolean $mInReferences
True when a <references> tag is being processed.
Definition Cite.php:162
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:1145
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:821
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:599
warning( $key, $param=null, $parse='parse')
Return a warning message based on a warning ID.
Definition Cite.php:1370
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:68
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
namespace being checked & $result
Definition hooks.txt:2385
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1873
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:2683
either a plain
Definition hooks.txt:2105
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:2055
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:2054
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 use $formDescriptor instead 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
$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
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang