MediaWiki REL1_33
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.ux-enhancements' );
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
241 private function guardedRef(
242 $str,
243 array $argv,
245 ) {
246 $this->mParser = $parser;
247
248 # The key here is the "name" attribute.
249 list( $key, $group, $follow, $dir ) = $this->refArg( $argv );
250 // empty string indicate invalid dir
251 if ( $dir === '' && $str !== '' ) {
252 $str .= $this->plainError( 'cite_error_ref_invalid_dir', $argv['dir'] );
253 }
254 # Split these into groups.
255 if ( $group === null ) {
256 if ( $this->mInReferences ) {
258 } else {
259 $group = self::DEFAULT_GROUP;
260 }
261 }
262
263 /*
264 * This section deals with constructions of the form
265 *
266 * <references>
267 * <ref name="foo"> BAR </ref>
268 * </references>
269 */
270 if ( $this->mInReferences ) {
271 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
272 if ( $group != $this->mReferencesGroup ) {
273 # <ref> and <references> have conflicting group attributes.
274 $this->mReferencesErrors[] =
275 $this->error(
276 'cite_error_references_group_mismatch',
277 Sanitizer::safeEncodeAttribute( $group )
278 );
279 } elseif ( $str !== '' ) {
280 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
281 # Called with group attribute not defined in text.
282 $this->mReferencesErrors[] =
283 $this->error(
284 'cite_error_references_missing_group',
285 Sanitizer::safeEncodeAttribute( $group )
286 );
287 } elseif ( $key === null || $key === '' ) {
288 # <ref> calls inside <references> must be named
289 $this->mReferencesErrors[] =
290 $this->error( 'cite_error_references_no_key' );
291 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
292 # Called with name attribute not defined in text.
293 $this->mReferencesErrors[] =
294 $this->error( 'cite_error_references_missing_key', Sanitizer::safeEncodeAttribute( $key ) );
295 } else {
296 if (
297 isset( $this->mRefs[$group][$key]['text'] ) &&
298 $str !== $this->mRefs[$group][$key]['text']
299 ) {
300 // two refs with same key and different content
301 // add error message to the original ref
302 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
303 'cite_error_references_duplicate_key', $key
304 );
305 } else {
306 # Assign the text to corresponding ref
307 $this->mRefs[$group][$key]['text'] = $str;
308 }
309 }
310 } else {
311 # <ref> called in <references> has no content.
312 $this->mReferencesErrors[] =
313 $this->error( 'cite_error_empty_references_define', Sanitizer::safeEncodeAttribute( $key ) );
314 }
315 return '';
316 }
317
318 if ( $str === '' ) {
319 # <ref ...></ref>. This construct is invalid if
320 # it's a contentful ref, but OK if it's a named duplicate and should
321 # be equivalent <ref ... />, for compatability with #tag.
322 if ( is_string( $key ) && $key !== '' ) {
323 $str = null;
324 } else {
325 $this->mRefCallStack[] = false;
326
327 return $this->error( 'cite_error_ref_no_input' );
328 }
329 }
330
331 if ( $key === false ) {
332 # TODO: Comment this case; what does this condition mean?
333 $this->mRefCallStack[] = false;
334 return $this->error( 'cite_error_ref_too_many_keys' );
335 }
336
337 if ( $str === null && $key === null ) {
338 # Something like <ref />; this makes no sense.
339 $this->mRefCallStack[] = false;
340 return $this->error( 'cite_error_ref_no_key' );
341 }
342
343 if ( is_string( $key ) && preg_match( '/^\d+$/', $key ) ||
344 is_string( $follow ) && preg_match( '/^\d+$/', $follow )
345 ) {
346 # Numeric names mess up the resulting id's, potentially produ-
347 # cing duplicate id's in the XHTML. The Right Thing To Do
348 # would be to mangle them, but it's not really high-priority
349 # (and would produce weird id's anyway).
350
351 $this->mRefCallStack[] = false;
352 return $this->error( 'cite_error_ref_numeric_key' );
353 }
354
355 if ( preg_match(
356 '/<ref\b[^<]*?>/',
357 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
358 ) ) {
359 # (bug T8199) This most likely implies that someone left off the
360 # closing </ref> tag, which will cause the entire article to be
361 # eaten up until the next <ref>. So we bail out early instead.
362 # The fancy regex above first tries chopping out anything that
363 # looks like a comment or SGML tag, which is a crude way to avoid
364 # false alarms for <nowiki>, <pre>, etc.
365
366 # Possible improvement: print the warning, followed by the contents
367 # of the <ref> tag. This way no part of the article will be eaten
368 # even temporarily.
369
370 $this->mRefCallStack[] = false;
371 return $this->error( 'cite_error_included_ref' );
372 }
373
374 if ( is_string( $key ) || is_string( $str ) ) {
375 # We don't care about the content: if the key exists, the ref
376 # is presumptively valid. Either it stores a new ref, or re-
377 # fers to an existing one. If it refers to a nonexistent ref,
378 # we'll figure that out later. Likewise it's definitely valid
379 # if there's any content, regardless of key.
380
381 return $this->stack( $str, $key, $group, $follow, $argv, $dir, $parser );
382 }
383
384 # Not clear how we could get here, but something is probably
385 # wrong with the types. Let's fail fast.
386 throw new Exception( 'Invalid $str and/or $key: ' . serialize( [ $str, $key ] ) );
387 }
388
402 private function refArg( array $argv ) {
403 $group = null;
404 $key = null;
405 $follow = null;
406 $dir = null;
407
408 if ( isset( $argv['dir'] ) ) {
409 // compare the dir attribute value against an explicit whitelist.
410 $dir = '';
411 $isValidDir = in_array( strtolower( $argv['dir'] ), [ 'ltr', 'rtl' ] );
412 if ( $isValidDir ) {
413 $dir = Html::expandAttributes( [ 'class' => 'mw-cite-dir-' . strtolower( $argv['dir'] ) ] );
414 }
415
416 unset( $argv['dir'] );
417 }
418
419 if ( $argv === [] ) {
420 // No key
421 return [ null, null, false, $dir ];
422 }
423
424 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
425 return [ false, false, false, false ];
426 }
427
428 if ( isset( $argv['name'] ) ) {
429 // Key given.
430 $key = trim( $argv['name'] );
431 unset( $argv['name'] );
432 }
433 if ( isset( $argv['follow'] ) ) {
434 // Follow given.
435 $follow = trim( $argv['follow'] );
436 unset( $argv['follow'] );
437 }
438 if ( isset( $argv['group'] ) ) {
439 // Group given.
440 $group = $argv['group'];
441 unset( $argv['group'] );
442 }
443
444 if ( $argv !== [] ) {
445 // Invalid key
446 return [ false, false, false, false ];
447 }
448
449 return [ $key, $group, $follow, $dir ];
450 }
451
466 private function stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser ) {
467 if ( !isset( $this->mRefs[$group] ) ) {
468 $this->mRefs[$group] = [];
469 }
470 if ( !isset( $this->mGroupCnt[$group] ) ) {
471 $this->mGroupCnt[$group] = 0;
472 }
473 if ( $follow != null ) {
474 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
475 // add text to the note that is being followed
476 $this->mRefs[$group][$follow]['text'] .= ' ' . $str;
477 } else {
478 // insert part of note at the beginning of the group
479 $groupsCount = count( $this->mRefs[$group] );
480 for ( $k = 0; $k < $groupsCount; $k++ ) {
481 if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
482 break;
483 }
484 }
485 array_splice( $this->mRefs[$group], $k, 0, [ [
486 'count' => -1,
487 'text' => $str,
488 'key' => ++$this->mOutCnt,
489 'follow' => $follow,
490 'dir' => $dir
491 ] ] );
492 array_splice( $this->mRefCallStack, $k, 0,
493 [ [ 'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
494 }
495 // return an empty string : this is not a reference
496 return '';
497 }
498
499 if ( $key === null ) {
500 // No key
501 // $this->mRefs[$group][] = $str;
502
503 $this->mRefs[$group][] = [
504 'count' => -1,
505 'text' => $str,
506 'key' => ++$this->mOutCnt,
507 'dir' => $dir
508 ];
509 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
510
511 return $this->linkRef( $group, $this->mOutCnt );
512 }
513 if ( !is_string( $key ) ) {
514 throw new Exception( 'Invalid stack key: ' . serialize( $key ) );
515 }
516
517 // Valid key
518 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
519 // First occurrence
520 $this->mRefs[$group][$key] = [
521 'text' => $str,
522 'count' => 0,
523 'key' => ++$this->mOutCnt,
524 'number' => ++$this->mGroupCnt[$group],
525 'dir' => $dir
526 ];
527 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
528
529 return $this->linkRef(
530 $group,
531 $key,
532 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
533 $this->mRefs[$group][$key]['number'],
534 "-" . $this->mRefs[$group][$key]['key']
535 );
536 }
537
538 // We've been here before
539 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
540 // If no text found before, use this text
541 $this->mRefs[$group][$key]['text'] = $str;
542 // Use the dir parameter only from the full definition of a named ref tag
543 $this->mRefs[$group][$key]['dir'] = $dir;
544 $this->mRefCallStack[] = [ 'assign', $call, $str, $key, $group,
545 $this->mRefs[$group][$key]['key'] ];
546 } else {
547 if ( $str != null && $str !== ''
548 // T205803 different strip markers might hide the same text
549 && $parser->mStripState->unstripBoth( $str )
550 !== $parser->mStripState->unstripBoth( $this->mRefs[$group][$key]['text'] )
551 ) {
552 // two refs with same key and different content
553 // add error message to the original ref
554 $this->mRefs[$group][$key]['text'] .= ' ' . $this->plainError(
555 'cite_error_references_duplicate_key', $key
556 );
557 }
558 $this->mRefCallStack[] = [ 'increment', $call, $str, $key, $group,
559 $this->mRefs[$group][$key]['key'] ];
560 }
561 return $this->linkRef(
562 $group,
563 $key,
564 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
565 $this->mRefs[$group][$key]['number'],
566 "-" . $this->mRefs[$group][$key]['key']
567 );
568 }
569
591 private function rollbackRef( $type, $key, $group, $index ) {
592 if ( !isset( $this->mRefs[$group] ) ) {
593 return;
594 }
595
596 if ( $key === null ) {
597 foreach ( $this->mRefs[$group] as $k => $v ) {
598 if ( $this->mRefs[$group][$k]['key'] === $index ) {
599 $key = $k;
600 break;
601 }
602 }
603 }
604
605 // Sanity checks that specified element exists.
606 if ( $key === null ) {
607 return;
608 }
609 if ( !isset( $this->mRefs[$group][$key] ) ) {
610 return;
611 }
612 if ( $this->mRefs[$group][$key]['key'] != $index ) {
613 return;
614 }
615
616 switch ( $type ) {
617 case 'new':
618 # Rollback the addition of new elements to the stack.
619 unset( $this->mRefs[$group][$key] );
620 if ( $this->mRefs[$group] === [] ) {
621 unset( $this->mRefs[$group] );
622 unset( $this->mGroupCnt[$group] );
623 }
624 break;
625 case 'assign':
626 # Rollback assignment of text to pre-existing elements.
627 $this->mRefs[$group][$key]['text'] = null;
628 # continue without break
629 case 'increment':
630 # Rollback increase in named ref occurrences.
631 $this->mRefs[$group][$key]['count']--;
632 break;
633 }
634 }
635
646 public function references( $str, array $argv, Parser $parser, PPFrame $frame ) {
647 if ( $this->mInCite || $this->mInReferences ) {
648 if ( $str === null ) {
649 return htmlspecialchars( "<references/>" );
650 }
651 return htmlspecialchars( "<references>$str</references>" );
652 }
653 $this->mCallCnt++;
654 $this->mInReferences = true;
655 $ret = $this->guardedReferences( $str, $argv, $parser );
656 $this->mInReferences = false;
657 $frame->setVolatile();
658 return $ret;
659 }
660
670 private function guardedReferences(
671 $str,
672 array $argv,
674 ) {
676
677 $this->mParser = $parser;
678
679 if ( isset( $argv['group'] ) ) {
680 $group = $argv['group'];
681 unset( $argv['group'] );
682 } else {
683 $group = self::DEFAULT_GROUP;
684 }
685
686 if ( strval( $str ) !== '' ) {
687 $this->mReferencesGroup = $group;
688
689 # Detect whether we were sent already rendered <ref>s.
690 # Mostly a side effect of using #tag to call references.
691 # The following assumes that the parsed <ref>s sent within
692 # the <references> block were the most recent calls to
693 # <ref>. This assumption is true for all known use cases,
694 # but not strictly enforced by the parser. It is possible
695 # that some unusual combination of #tag, <references> and
696 # conditional parser functions could be created that would
697 # lead to malformed references here.
698 $count = substr_count( $str, Parser::MARKER_PREFIX . "-ref-" );
699 $redoStack = [];
700
701 # Undo effects of calling <ref> while unaware of containing <references>
702 for ( $i = 1; $i <= $count; $i++ ) {
703 if ( !$this->mRefCallStack ) {
704 break;
705 }
706
707 $call = array_pop( $this->mRefCallStack );
708 $redoStack[] = $call;
709 if ( $call !== false ) {
710 list( $type, $ref_argv, $ref_str,
711 $ref_key, $ref_group, $ref_index ) = $call;
712 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
713 }
714 }
715
716 # Rerun <ref> call now that mInReferences is set.
717 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
718 $call = $redoStack[$i];
719 if ( $call !== false ) {
720 list( $type, $ref_argv, $ref_str,
721 $ref_key, $ref_group, $ref_index ) = $call;
722 $this->guardedRef( $ref_str, $ref_argv, $parser );
723 }
724 }
725
726 # Parse $str to process any unparsed <ref> tags.
727 $parser->recursiveTagParse( $str );
728
729 # Reset call stack
730 $this->mRefCallStack = [];
731 }
732
733 if ( isset( $argv['responsive'] ) ) {
734 $responsive = $argv['responsive'] !== '0';
735 unset( $argv['responsive'] );
736 } else {
737 $responsive = $wgCiteResponsiveReferences;
738 }
739
740 // There are remaining parameters we don't recognise
741 if ( $argv ) {
742 return $this->error( 'cite_error_references_invalid_parameters' );
743 }
744
745 $s = $this->referencesFormat( $group, $responsive );
746
747 # Append errors generated while processing <references>
748 if ( $this->mReferencesErrors ) {
749 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
750 $this->mReferencesErrors = [];
751 }
752 return $s;
753 }
754
765 private function referencesFormat( $group, $responsive ) {
766 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
767 return '';
768 }
769
770 $ent = [];
771 foreach ( $this->mRefs[$group] as $k => $v ) {
772 $ent[] = $this->referencesFormatEntry( $k, $v );
773 }
774
775 // Add new lines between the list items (ref entires) to avoid confusing tidy (T15073).
776 // Note: This builds a string of wikitext, not html.
777 $parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ],
778 "\n" . implode( "\n", $ent ) . "\n"
779 );
780
781 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
782 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" );
783
784 if ( $responsive ) {
785 // Use a DIV wrap because column-count on a list directly is broken in Chrome.
786 // See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
787 $wrapClasses = [ 'mw-references-wrap' ];
788 if ( count( $this->mRefs[$group] ) > 10 ) {
789 $wrapClasses[] = 'mw-references-columns';
790 }
791 $ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
792 }
793
794 if ( !$this->mParser->getOptions()->getIsPreview() ) {
795 // save references data for later use by LinksUpdate hooks
796 $this->saveReferencesData( $group );
797 }
798
799 // done, clean up so we can reuse the group
800 unset( $this->mRefs[$group] );
801 unset( $this->mGroupCnt[$group] );
802
803 return $ret;
804 }
805
814 private function referencesFormatEntry( $key, $val ) {
815 // Anonymous reference
816 if ( !is_array( $val ) ) {
817 return wfMessage(
818 'cite_references_link_one',
819 $this->normalizeKey(
820 self::getReferencesKey( $key )
821 ),
822 $this->normalizeKey(
823 $this->refKey( $key )
824 ),
825 $this->referenceText( $key, $val ),
826 $val['dir']
827 )->inContentLanguage()->plain();
828 }
829 $text = $this->referenceText( $key, $val['text'] );
830 if ( isset( $val['follow'] ) ) {
831 return wfMessage(
832 'cite_references_no_link',
833 $this->normalizeKey(
834 self::getReferencesKey( $val['follow'] )
835 ),
836 $text
837 )->inContentLanguage()->plain();
838 }
839 if ( !isset( $val['count'] ) ) {
840 // this handles the case of section preview for list-defined references
841 return wfMessage( 'cite_references_link_many',
842 $this->normalizeKey(
843 self::getReferencesKey( $key . "-" . ( $val['key'] ?? '' ) )
844 ),
845 '',
846 $text
847 )->inContentLanguage()->plain();
848 }
849 if ( $val['count'] < 0 ) {
850 return wfMessage(
851 'cite_references_link_one',
852 $this->normalizeKey(
853 self::getReferencesKey( $val['key'] )
854 ),
855 $this->normalizeKey(
856 # $this->refKey( $val['key'], $val['count'] )
857 $this->refKey( $val['key'] )
858 ),
859 $text,
860 $val['dir']
861 )->inContentLanguage()->plain();
862 // Standalone named reference, I want to format this like an
863 // anonymous reference because displaying "1. 1.1 Ref text" is
864 // overkill and users frequently use named references when they
865 // don't need them for convenience
866 }
867 if ( $val['count'] === 0 ) {
868 return wfMessage(
869 'cite_references_link_one',
870 $this->normalizeKey(
871 self::getReferencesKey( $key . "-" . $val['key'] )
872 ),
873 $this->normalizeKey(
874 # $this->refKey( $key, $val['count'] ),
875 $this->refKey( $key, $val['key'] . "-" . $val['count'] )
876 ),
877 $text,
878 $val['dir']
879 )->inContentLanguage()->plain();
880 // Named references with >1 occurrences
881 }
882 $links = [];
883 // for group handling, we have an extra key here.
884 for ( $i = 0; $i <= $val['count']; ++$i ) {
885 $links[] = wfMessage(
886 'cite_references_link_many_format',
887 $this->normalizeKey(
888 $this->refKey( $key, $val['key'] . "-$i" )
889 ),
890 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
892 )->inContentLanguage()->plain();
893 }
894
895 $list = $this->listToText( $links );
896
897 return wfMessage( 'cite_references_link_many',
898 $this->normalizeKey(
899 self::getReferencesKey( $key . "-" . $val['key'] )
900 ),
901 $list,
902 $text,
903 $val['dir']
904 )->inContentLanguage()->plain();
905 }
906
913 private function referenceText( $key, $text ) {
914 if ( $text === null || $text === '' ) {
915 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
916 return $this->warning( 'cite_warning_sectionpreview_no_text', $key, 'noparse' );
917 }
918 return $this->plainError( 'cite_error_references_no_text', $key );
919 }
920 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
921 }
922
933 private function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
934 global $wgContLang;
935 $scope = strlen( $max );
936 $ret = $wgContLang->formatNum(
937 sprintf( "%s.%0{$scope}s", $base, $offset )
938 );
939 return $ret;
940 }
941
953 if ( !isset( $this->mBacklinkLabels ) ) {
954 $this->genBacklinkLabels();
955 }
956 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
957 return $this->mBacklinkLabels[$offset];
958 } else {
959 // Feed me!
960 return $this->plainError( 'cite_error_references_no_backlink_label', null );
961 }
962 }
963
976 private function getLinkLabel( $offset, $group, $label ) {
977 $message = "cite_link_label_group-$group";
978 if ( !isset( $this->mLinkLabels[$group] ) ) {
979 $this->genLinkLabels( $group, $message );
980 }
981 if ( $this->mLinkLabels[$group] === false ) {
982 // Use normal representation, ie. "$group 1", "$group 2"...
983 return $label;
984 }
985
986 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
987 return $this->mLinkLabels[$group][$offset - 1];
988 } else {
989 // Feed me!
990 return $this->plainError( 'cite_error_no_link_label_group', [ $group, $message ] );
991 }
992 }
993
1003 private function refKey( $key, $num = null ) {
1004 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text();
1005 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text();
1006 if ( $num !== null ) {
1007 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num )
1008 ->inContentLanguage()->plain();
1009 }
1010
1011 return "$prefix$key$suffix";
1012 }
1013
1022 public static function getReferencesKey( $key ) {
1023 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text();
1024 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text();
1025
1026 return "$prefix$key$suffix";
1027 }
1028
1045 private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
1046 global $wgContLang;
1047
1048 if ( $label === null ) {
1049 $label = ++$this->mGroupCnt[$group];
1050 }
1051
1052 return $this->mParser->recursiveTagParse(
1053 wfMessage(
1054 'cite_reference_link',
1055 $this->normalizeKey(
1056 $this->refKey( $key, $count )
1057 ),
1058 $this->normalizeKey(
1059 self::getReferencesKey( $key . $subkey )
1060 ),
1061 Sanitizer::safeEncodeAttribute(
1062 $this->getLinkLabel( $label, $group,
1063 ( ( $group === self::DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) )
1064 )
1065 )->inContentLanguage()->plain()
1066 );
1067 }
1068
1075 private function normalizeKey( $key ) {
1076 $key = Sanitizer::escapeIdForAttribute( $key );
1077 $key = preg_replace( '/__+/', '_', $key );
1078 $key = Sanitizer::safeEncodeAttribute( $key );
1079
1080 return $key;
1081 }
1082
1093 private function listToText( $arr ) {
1094 $cnt = count( $arr );
1095 if ( $cnt === 1 ) {
1096 // Enforce always returning a string
1097 return (string)$arr[0];
1098 }
1099
1100 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain();
1101 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain();
1102 $t = array_slice( $arr, 0, $cnt - 1 );
1103 return implode( $sep, $t ) . $and . $arr[$cnt - 1];
1104 }
1105
1111 private function genBacklinkLabels() {
1112 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' )
1113 ->inContentLanguage()->plain();
1114 $this->mBacklinkLabels = preg_split( '/\s+/', $text );
1115 }
1116
1125 private function genLinkLabels( $group, $message ) {
1126 $text = false;
1127 $msg = wfMessage( $message )->inContentLanguage();
1128 if ( $msg->exists() ) {
1129 $text = $msg->plain();
1130 }
1131 $this->mLinkLabels[$group] = $text ? preg_split( '/\s+/', $text ) : false;
1132 }
1133
1140 public function clearState( Parser $parser ) {
1141 if ( $parser->extCite !== $this ) {
1142 $parser->extCite->clearState( $parser );
1143 return;
1144 }
1145
1146 # Don't clear state when we're in the middle of parsing
1147 # a <ref> tag
1148 if ( $this->mInCite || $this->mInReferences ) {
1149 return;
1150 }
1151
1152 $this->mGroupCnt = [];
1153 $this->mOutCnt = 0;
1154 $this->mCallCnt = 0;
1155 $this->mRefs = [];
1156 $this->mReferencesErrors = [];
1157 $this->mRefCallStack = [];
1158 }
1159
1165 public function cloneState( Parser $parser ) {
1166 if ( $parser->extCite !== $this ) {
1167 $parser->extCite->cloneState( $parser );
1168 return;
1169 }
1170
1171 $parser->extCite = clone $this;
1172 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1173 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1174
1175 // Clear the state, making sure it will actually work.
1176 $parser->extCite->mInCite = false;
1177 $parser->extCite->mInReferences = false;
1178 $parser->extCite->clearState( $parser );
1179 }
1180
1193 public function checkRefsNoReferences( $afterParse, $parser, &$text ) {
1195 if ( $parser->extCite === null ) {
1196 return;
1197 }
1198 if ( $parser->extCite !== $this ) {
1199 $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1200 return;
1201 }
1202
1203 if ( $afterParse ) {
1204 $this->mHaveAfterParse = true;
1205 } elseif ( $this->mHaveAfterParse ) {
1206 return;
1207 }
1208
1209 if ( !$parser->getOptions()->getIsPreview() ) {
1210 // save references data for later use by LinksUpdate hooks
1211 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1212 $this->saveReferencesData();
1213 }
1214 $isSectionPreview = false;
1215 } else {
1216 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1217 }
1218
1219 $s = '';
1220 foreach ( $this->mRefs as $group => $refs ) {
1221 if ( !$refs ) {
1222 continue;
1223 }
1224 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1225 $this->mInReferences = true;
1226 $s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
1227 $this->mInReferences = false;
1228 } else {
1229 $s .= "\n<br />" .
1230 $this->error(
1231 'cite_error_group_refs_without_references',
1232 Sanitizer::safeEncodeAttribute( $group )
1233 );
1234 }
1235 }
1236 if ( $isSectionPreview && $s !== '' ) {
1237 // provide a preview of references in its own section
1238 $text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
1239 $headerMsg = wfMessage( 'cite_section_preview_references' );
1240 if ( !$headerMsg->isDisabled() ) {
1241 $text .= '<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1242 . $headerMsg->escaped()
1243 . '</h2>';
1244 }
1245 $text .= $s . '</div>';
1246 } else {
1247 $text .= $s;
1248 }
1249 }
1250
1258 private function saveReferencesData( $group = self::DEFAULT_GROUP ) {
1260 if ( !$wgCiteStoreReferencesData ) {
1261 return;
1262 }
1263 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1264 if ( $savedRefs === null ) {
1265 // Initialize array structure
1266 $savedRefs = [
1267 'refs' => [],
1268 'version' => self::DATA_VERSION_NUMBER,
1269 ];
1270 }
1271 if ( $this->mBumpRefData ) {
1272 // This handles pages with multiple <references/> tags with <ref> tags in between.
1273 // On those, a group can appear several times, so we need to avoid overwriting
1274 // a previous appearance.
1275 $savedRefs['refs'][] = [];
1276 $this->mBumpRefData = false;
1277 }
1278 $n = count( $savedRefs['refs'] ) - 1;
1279 // save group
1280 $savedRefs['refs'][$n][$group] = $this->mRefs[$group];
1281
1282 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1283 }
1284
1290 public static function setHooks( Parser $parser ) {
1291 global $wgHooks;
1292
1293 $parser->extCite = new self();
1294
1295 if ( !self::$hooksInstalled ) {
1296 $wgHooks['ParserClearState'][] = [ $parser->extCite, 'clearState' ];
1297 $wgHooks['ParserCloned'][] = [ $parser->extCite, 'cloneState' ];
1298 $wgHooks['ParserAfterParse'][] = [ $parser->extCite, 'checkRefsNoReferences', true ];
1299 $wgHooks['ParserBeforeTidy'][] = [ $parser->extCite, 'checkRefsNoReferences', false ];
1300 self::$hooksInstalled = true;
1301 }
1302 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1303 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1304 }
1305
1313 private function error( $key, $param = null ) {
1314 $error = $this->plainError( $key, $param );
1315 return $this->mParser->recursiveTagParse( $error );
1316 }
1317
1326 private function plainError( $key, $param = null ) {
1327 # For ease of debugging and because errors are rare, we
1328 # use the user language and split the parser cache.
1329 $lang = $this->mParser->getOptions()->getUserLangObj();
1330 $dir = $lang->getDir();
1331
1332 # We rely on the fact that PHP is okay with passing unused argu-
1333 # ments to functions. If $1 is not used in the message, wfMessage will
1334 # just ignore the extra parameter.
1335 $msg = wfMessage(
1336 'cite_error',
1337 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1338 )
1339 ->inLanguage( $lang )
1340 ->plain();
1341
1342 $this->mParser->addTrackingCategory( 'cite-tracking-category-cite-error' );
1343
1344 $ret = Html::rawElement(
1345 'span',
1346 [
1347 'class' => 'error mw-ext-cite-error',
1348 'lang' => $lang->getHtmlCode(),
1349 'dir' => $dir,
1350 ],
1351 $msg
1352 );
1353
1354 return $ret;
1355 }
1356
1365 private function warning( $key, $param = null, $parse = 'parse' ) {
1366 # For ease of debugging and because errors are rare, we
1367 # use the user language and split the parser cache.
1368 $lang = $this->mParser->getOptions()->getUserLangObj();
1369 $dir = $lang->getDir();
1370
1371 # We rely on the fact that PHP is okay with passing unused argu-
1372 # ments to functions. If $1 is not used in the message, wfMessage will
1373 # just ignore the extra parameter.
1374 $msg = wfMessage(
1375 'cite_warning',
1376 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1377 )
1378 ->inLanguage( $lang )
1379 ->plain();
1380
1381 $key = preg_replace( '/^cite_warning_/', '', $key ) . '';
1382 $ret = Html::rawElement(
1383 'span',
1384 [
1385 'class' => 'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1386 Sanitizer::escapeClass( $key ),
1387 'lang' => $lang->getHtmlCode(),
1388 'dir' => $dir,
1389 ],
1390 $msg
1391 );
1392
1393 if ( $parse === 'parse' ) {
1394 $ret = $this->mParser->recursiveTagParse( $ret );
1395 }
1396
1397 return $ret;
1398 }
1399
1407 public static function getStoredReferences( Title $title ) {
1409 if ( !$wgCiteStoreReferencesData ) {
1410 return false;
1411 }
1412 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1413 $key = $cache->makeKey( self::EXT_DATA_KEY, $title->getArticleID() );
1414 return $cache->getWithSetCallback(
1415 $key,
1416 self::CACHE_DURATION_ONFETCH,
1417 function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
1418 $dbr = wfGetDB( DB_REPLICA );
1419 $setOpts += Database::getCacheSetOptions( $dbr );
1420 return self::recursiveFetchRefsFromDB( $title, $dbr );
1421 },
1422 [
1423 'checkKeys' => [ $key ],
1424 'lockTSE' => 30,
1425 ]
1426 );
1427 }
1428
1440 private static function recursiveFetchRefsFromDB( Title $title, IDatabase $dbr,
1441 $string = '', $i = 1 ) {
1442 $id = $title->getArticleID();
1443 $result = $dbr->selectField(
1444 'page_props',
1445 'pp_value',
1446 [
1447 'pp_page' => $id,
1448 'pp_propname' => 'references-' . $i
1449 ],
1450 __METHOD__
1451 );
1452 if ( $result !== false ) {
1453 $string .= $result;
1454 $decodedString = gzdecode( $string );
1455 if ( $decodedString !== false ) {
1456 $json = json_decode( $decodedString, true );
1457 if ( json_last_error() === JSON_ERROR_NONE ) {
1458 return $json;
1459 }
1460 // corrupted json ?
1461 // shouldn't happen since when string is truncated, gzdecode should fail
1462 wfDebug( "Corrupted json detected when retrieving stored references for title id $id" );
1463 }
1464 // if gzdecode fails, try to fetch next references- property value
1465 return self::recursiveFetchRefsFromDB( $title, $dbr, $string, ++$i );
1466
1467 } else {
1468 // no refs stored in page_props at this index
1469 if ( $i > 1 ) {
1470 // shouldn't happen
1471 wfDebug( "Failed to retrieve stored references for title id $id" );
1472 }
1473 return false;
1474 }
1475 }
1476
1477}
serialize()
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
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:790
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:976
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:402
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:1045
referenceText( $key, $text)
Returns formatted reference text.
Definition Cite.php:913
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:1125
genBacklinkLabels()
Generate the labels to pass to the 'cite_references_link_many_format' message, the format is an arbit...
Definition Cite.php:1111
stack( $str, $key, $group, $follow, array $call, $dir, Parser $parser)
Populate $this->mRefs based on input and arguments to <ref>
Definition Cite.php:466
references( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <references>
Definition Cite.php:646
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:1258
error( $key, $param=null)
Return an error message based on an error ID and parses it.
Definition Cite.php:1313
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:1093
string[] $mBacklinkLabels
The backlinks, in order, to pass as $3 to 'cite_references_link_many_format', defined in 'cite_refere...
Definition Cite.php:126
static getStoredReferences(Title $title)
Fetch references stored for the given title in page_props For performance, results are cached.
Definition Cite.php:1407
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:1003
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:1440
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:1326
cloneState(Parser $parser)
Gets run when the parser is cloned.
Definition Cite.php:1165
referencesFormatEntryAlternateBacklinkLabel( $offset)
Generate a custom format backlink given an offset, e.g.
Definition Cite.php:952
referencesFormat( $group, $responsive)
Make output to be returned from the references() function.
Definition Cite.php:765
string[] false[] $mLinkLabels
The links to use per group, in order.
Definition Cite.php:133
array false[] $mRefCallStack
<ref> call stack Used to cleanup out of sequence ref calls created by #tag See description of functio...
Definition Cite.php:185
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:1193
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:933
normalizeKey( $key)
Normalizes and sanitizes a reference key.
Definition Cite.php:1075
guardedRef( $str, array $argv, Parser $parser)
Definition Cite.php:241
static setHooks(Parser $parser)
Initialize the parser hooks.
Definition Cite.php:1290
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:1022
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:1140
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:814
guardedReferences( $str, array $argv, Parser $parser)
Must only be called from references().
Definition Cite.php:670
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:591
warning( $key, $param=null, $parse='parse')
Return a warning message based on a warning ID.
Definition Cite.php:1365
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:69
Represents a title within MediaWiki.
Definition Title.php:40
Relational database abstraction object.
Definition Database.php:49
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:2340
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1834
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:2644
either a plain
Definition hooks.txt:2054
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:2004
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:2003
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
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