MediaWiki REL1_30
Cite.php
Go to the documentation of this file.
1<?php
2
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.a11y' );
218 $parserOutput->addModuleStyles( 'ext.cite.styles' );
219
220 if ( is_callable( [ $frame, 'setVolatile' ] ) ) {
221 $frame->setVolatile();
222 }
223
224 // new <ref> tag, we may need to bump the ref data counter
225 // to avoid overwriting a previous group
226 $this->mBumpRefData = true;
227
228 return $ret;
229 }
230
240 private function guardedRef(
241 $str,
242 array $argv,
244 $default_group = self::DEFAULT_GROUP
245 ) {
246 $this->mParser = $parser;
247
248 # The key here is the "name" attribute.
249 list( $key, $group, $follow ) = $this->refArg( $argv );
250
251 # Split these into groups.
252 if ( $group === null ) {
253 if ( $this->mInReferences ) {
255 } else {
256 $group = $default_group;
257 }
258 }
259
260 /*
261 * This section deals with constructions of the form
262 *
263 * <references>
264 * <ref name="foo"> BAR </ref>
265 * </references>
266 */
267 if ( $this->mInReferences ) {
268 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
269 if ( $group != $this->mReferencesGroup ) {
270 # <ref> and <references> have conflicting group attributes.
271 $this->mReferencesErrors[] =
272 $this->error( 'cite_error_references_group_mismatch', htmlspecialchars( $group ) );
273 } elseif ( $str !== '' ) {
274 if ( !$isSectionPreview && !isset( $this->mRefs[$group] ) ) {
275 # Called with group attribute not defined in text.
276 $this->mReferencesErrors[] =
277 $this->error( 'cite_error_references_missing_group', htmlspecialchars( $group ) );
278 } elseif ( $key === null || $key === '' ) {
279 # <ref> calls inside <references> must be named
280 $this->mReferencesErrors[] =
281 $this->error( 'cite_error_references_no_key' );
282 } elseif ( !$isSectionPreview && !isset( $this->mRefs[$group][$key] ) ) {
283 # Called with name attribute not defined in text.
284 $this->mReferencesErrors[] =
285 $this->error( 'cite_error_references_missing_key', $key );
286 } else {
287 if (
288 isset( $this->mRefs[$group][$key]['text'] ) &&
289 $str !== $this->mRefs[$group][$key]['text']
290 ) {
291 // two refs with same key and different content
292 // add error message to the original ref
293 $this->mRefs[$group][$key]['text'] .= ' ' . $this->error(
294 'cite_error_references_duplicate_key', $key, 'noparse'
295 );
296 } else {
297 # Assign the text to corresponding ref
298 $this->mRefs[$group][$key]['text'] = $str;
299 }
300 }
301 } else {
302 # <ref> called in <references> has no content.
303 $this->mReferencesErrors[] =
304 $this->error( 'cite_error_empty_references_define', $key );
305 }
306 return '';
307 }
308
309 if ( $str === '' ) {
310 # <ref ...></ref>. This construct is invalid if
311 # it's a contentful ref, but OK if it's a named duplicate and should
312 # be equivalent <ref ... />, for compatability with #tag.
313 if ( is_string( $key ) && $key !== '' ) {
314 $str = null;
315 } else {
316 $this->mRefCallStack[] = false;
317
318 return $this->error( 'cite_error_ref_no_input' );
319 }
320 }
321
322 if ( $key === false ) {
323 # TODO: Comment this case; what does this condition mean?
324 $this->mRefCallStack[] = false;
325 return $this->error( 'cite_error_ref_too_many_keys' );
326 }
327
328 if ( $str === null && $key === null ) {
329 # Something like <ref />; this makes no sense.
330 $this->mRefCallStack[] = false;
331 return $this->error( 'cite_error_ref_no_key' );
332 }
333
334 if ( preg_match( '/^[0-9]+$/', $key ) || preg_match( '/^[0-9]+$/', $follow ) ) {
335 # Numeric names mess up the resulting id's, potentially produ-
336 # cing duplicate id's in the XHTML. The Right Thing To Do
337 # would be to mangle them, but it's not really high-priority
338 # (and would produce weird id's anyway).
339
340 $this->mRefCallStack[] = false;
341 return $this->error( 'cite_error_ref_numeric_key' );
342 }
343
344 if ( preg_match(
345 '/<ref\b[^<]*?>/',
346 preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
347 ) ) {
348 # (bug T8199) This most likely implies that someone left off the
349 # closing </ref> tag, which will cause the entire article to be
350 # eaten up until the next <ref>. So we bail out early instead.
351 # The fancy regex above first tries chopping out anything that
352 # looks like a comment or SGML tag, which is a crude way to avoid
353 # false alarms for <nowiki>, <pre>, etc.
354
355 # Possible improvement: print the warning, followed by the contents
356 # of the <ref> tag. This way no part of the article will be eaten
357 # even temporarily.
358
359 $this->mRefCallStack[] = false;
360 return $this->error( 'cite_error_included_ref' );
361 }
362
363 if ( is_string( $key ) || is_string( $str ) ) {
364 # We don't care about the content: if the key exists, the ref
365 # is presumptively valid. Either it stores a new ref, or re-
366 # fers to an existing one. If it refers to a nonexistent ref,
367 # we'll figure that out later. Likewise it's definitely valid
368 # if there's any content, regardless of key.
369
370 return $this->stack( $str, $key, $group, $follow, $argv );
371 }
372
373 # Not clear how we could get here, but something is probably
374 # wrong with the types. Let's fail fast.
375 throw new Exception( 'Invalid $str and/or $key: ' . serialize( [ $str, $key ] ) );
376 }
377
390 private function refArg( array $argv ) {
391 $cnt = count( $argv );
392 $group = null;
393 $key = null;
394 $follow = null;
395
396 if ( $cnt > 2 ) {
397 // There should only be one key or follow parameter, and one group parameter
398 // FIXME : this looks inconsistent, it should probably return a tuple
399 return false;
400 } elseif ( $cnt >= 1 ) {
401 if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
402 return [ false, false, false ];
403 }
404 if ( isset( $argv['name'] ) ) {
405 // Key given.
406 $key = Sanitizer::escapeId( $argv['name'], 'noninitial' );
407 unset( $argv['name'] );
408 --$cnt;
409 }
410 if ( isset( $argv['follow'] ) ) {
411 // Follow given.
412 $follow = Sanitizer::escapeId( $argv['follow'], 'noninitial' );
413 unset( $argv['follow'] );
414 --$cnt;
415 }
416 if ( isset( $argv['group'] ) ) {
417 // Group given.
418 $group = $argv['group'];
419 unset( $argv['group'] );
420 --$cnt;
421 }
422
423 if ( $cnt === 0 ) {
424 return [ $key, $group, $follow ];
425 } else {
426 // Invalid key
427 return [ false, false, false ];
428 }
429 } else {
430 // No key
431 return [ null, $group, false ];
432 }
433 }
434
447 private function stack( $str, $key = null, $group, $follow, array $call ) {
448 if ( !isset( $this->mRefs[$group] ) ) {
449 $this->mRefs[$group] = [];
450 }
451 if ( !isset( $this->mGroupCnt[$group] ) ) {
452 $this->mGroupCnt[$group] = 0;
453 }
454 if ( $follow != null ) {
455 if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
456 // add text to the note that is being followed
457 $this->mRefs[$group][$follow]['text'] .= ' ' . $str;
458 } else {
459 // insert part of note at the beginning of the group
460 $groupsCount = count( $this->mRefs[$group] );
461 for ( $k = 0; $k < $groupsCount; $k++ ) {
462 if ( !isset( $this->mRefs[$group][$k]['follow'] ) ) {
463 break;
464 }
465 }
466 array_splice( $this->mRefs[$group], $k, 0, [ [
467 'count' => -1,
468 'text' => $str,
469 'key' => ++$this->mOutCnt,
470 'follow' => $follow
471 ] ] );
472 array_splice( $this->mRefCallStack, $k, 0,
473 [ [ 'new', $call, $str, $key, $group, $this->mOutCnt ] ] );
474 }
475 // return an empty string : this is not a reference
476 return '';
477 }
478
479 if ( $key === null ) {
480 // No key
481 // $this->mRefs[$group][] = $str;
482 $this->mRefs[$group][] = [
483 'count' => -1,
484 'text' => $str,
485 'key' => ++$this->mOutCnt
486 ];
487 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
488
489 return $this->linkRef( $group, $this->mOutCnt );
490 }
491 if ( !is_string( $key ) ) {
492 throw new Exception( 'Invalid stack key: ' . serialize( $key ) );
493 }
494
495 // Valid key
496 if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
497 // First occurrence
498 $this->mRefs[$group][$key] = [
499 'text' => $str,
500 'count' => 0,
501 'key' => ++$this->mOutCnt,
502 'number' => ++$this->mGroupCnt[$group]
503 ];
504 $this->mRefCallStack[] = [ 'new', $call, $str, $key, $group, $this->mOutCnt ];
505
506 return $this->linkRef(
507 $group,
508 $key,
509 $this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
510 $this->mRefs[$group][$key]['number'],
511 "-" . $this->mRefs[$group][$key]['key']
512 );
513 }
514
515 // We've been here before
516 if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
517 // If no text found before, use this text
518 $this->mRefs[$group][$key]['text'] = $str;
519 $this->mRefCallStack[] = [ 'assign', $call, $str, $key, $group,
520 $this->mRefs[$group][$key]['key'] ];
521 } else {
522 if ( $str != null && $str !== '' && $str !== $this->mRefs[$group][$key]['text'] ) {
523 // two refs with same key and different content
524 // add error message to the original ref
525 $this->mRefs[$group][$key]['text'] .= ' ' . $this->error(
526 'cite_error_references_duplicate_key', $key, 'noparse'
527 );
528 }
529 $this->mRefCallStack[] = [ 'increment', $call, $str, $key, $group,
530 $this->mRefs[$group][$key]['key'] ];
531 }
532 return $this->linkRef(
533 $group,
534 $key,
535 $this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
536 $this->mRefs[$group][$key]['number'],
537 "-" . $this->mRefs[$group][$key]['key']
538 );
539 }
540
562 private function rollbackRef( $type, $key, $group, $index ) {
563 if ( !isset( $this->mRefs[$group] ) ) {
564 return;
565 }
566
567 if ( $key === null ) {
568 foreach ( $this->mRefs[$group] as $k => $v ) {
569 if ( $this->mRefs[$group][$k]['key'] === $index ) {
570 $key = $k;
571 break;
572 }
573 }
574 }
575
576 // Sanity checks that specified element exists.
577 if ( $key === null ) {
578 return;
579 }
580 if ( !isset( $this->mRefs[$group][$key] ) ) {
581 return;
582 }
583 if ( $this->mRefs[$group][$key]['key'] != $index ) {
584 return;
585 }
586
587 switch ( $type ) {
588 case 'new':
589 # Rollback the addition of new elements to the stack.
590 unset( $this->mRefs[$group][$key] );
591 if ( $this->mRefs[$group] === [] ) {
592 unset( $this->mRefs[$group] );
593 unset( $this->mGroupCnt[$group] );
594 }
595 break;
596 case 'assign':
597 # Rollback assignment of text to pre-existing elements.
598 $this->mRefs[$group][$key]['text'] = null;
599 # continue without break
600 case 'increment':
601 # Rollback increase in named ref occurrences.
602 $this->mRefs[$group][$key]['count']--;
603 break;
604 }
605 }
606
617 public function references( $str, array $argv, Parser $parser, PPFrame $frame ) {
618 if ( $this->mInCite || $this->mInReferences ) {
619 if ( is_null( $str ) ) {
620 return htmlspecialchars( "<references/>" );
621 }
622 return htmlspecialchars( "<references>$str</references>" );
623 }
624 $this->mCallCnt++;
625 $this->mInReferences = true;
626 $ret = $this->guardedReferences( $str, $argv, $parser );
627 $this->mInReferences = false;
628 if ( is_callable( [ $frame, 'setVolatile' ] ) ) {
629 $frame->setVolatile();
630 }
631 return $ret;
632 }
633
642 private function guardedReferences(
643 $str,
644 array $argv,
646 $group = self::DEFAULT_GROUP
647 ) {
648 global $wgCiteResponsiveReferences;
649
650 $this->mParser = $parser;
651
652 if ( isset( $argv['group'] ) ) {
653 $group = $argv['group'];
654 unset( $argv['group'] );
655 }
656
657 if ( strval( $str ) !== '' ) {
658 $this->mReferencesGroup = $group;
659
660 # Detect whether we were sent already rendered <ref>s.
661 # Mostly a side effect of using #tag to call references.
662 # The following assumes that the parsed <ref>s sent within
663 # the <references> block were the most recent calls to
664 # <ref>. This assumption is true for all known use cases,
665 # but not strictly enforced by the parser. It is possible
666 # that some unusual combination of #tag, <references> and
667 # conditional parser functions could be created that would
668 # lead to malformed references here.
669 $count = substr_count( $str, Parser::MARKER_PREFIX . "-ref-" );
670 $redoStack = [];
671
672 # Undo effects of calling <ref> while unaware of containing <references>
673 for ( $i = 1; $i <= $count; $i++ ) {
674 if ( !$this->mRefCallStack ) {
675 break;
676 }
677
678 $call = array_pop( $this->mRefCallStack );
679 $redoStack[] = $call;
680 if ( $call !== false ) {
681 list( $type, $ref_argv, $ref_str,
682 $ref_key, $ref_group, $ref_index ) = $call;
683 $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
684 }
685 }
686
687 # Rerun <ref> call now that mInReferences is set.
688 for ( $i = count( $redoStack ) - 1; $i >= 0; $i-- ) {
689 $call = $redoStack[$i];
690 if ( $call !== false ) {
691 list( $type, $ref_argv, $ref_str,
692 $ref_key, $ref_group, $ref_index ) = $call;
693 $this->guardedRef( $ref_str, $ref_argv, $parser );
694 }
695 }
696
697 # Parse $str to process any unparsed <ref> tags.
698 $parser->recursiveTagParse( $str );
699
700 # Reset call stack
701 $this->mRefCallStack = [];
702 }
703
704 if ( isset( $argv['responsive'] ) ) {
705 $responsive = $argv['responsive'] !== '0';
706 unset( $argv['responsive'] );
707 } else {
708 $responsive = $wgCiteResponsiveReferences;
709 }
710
711 // There are remaining parameters we don't recognise
712 if ( $argv ) {
713 return $this->error( 'cite_error_references_invalid_parameters' );
714 }
715
716 $s = $this->referencesFormat( $group, $responsive );
717
718 # Append errors generated while processing <references>
719 if ( $this->mReferencesErrors ) {
720 $s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
721 $this->mReferencesErrors = [];
722 }
723 return $s;
724 }
725
733 private function referencesFormat( $group, $responsive ) {
734 if ( !$this->mRefs || !isset( $this->mRefs[$group] ) ) {
735 return '';
736 }
737
738 $ent = [];
739 foreach ( $this->mRefs[$group] as $k => $v ) {
740 $ent[] = $this->referencesFormatEntry( $k, $v );
741 }
742
743 // Add new lines between the list items (ref entires) to avoid confusing tidy (bug 13073).
744 // Note: This builds a string of wikitext, not html.
745 $parserInput = Html::rawElement( 'ol', [ 'class' => [ 'references' ] ],
746 "\n" . implode( "\n", $ent ) . "\n"
747 );
748
749 // Let's try to cache it.
750 global $wgCiteCacheReferences, $wgMemc;
751 $data = false;
752 if ( $wgCiteCacheReferences ) {
753 $cacheKey = wfMemcKey(
754 'citeref',
755 md5( $parserInput ),
756 $this->mParser->Title()->getArticleID()
757 );
758 $data = $wgMemc->get( $cacheKey );
759 }
760
761 if ( !$data || !$this->mParser->isValidHalfParsedText( $data ) ) {
762 // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar
763 $ret = rtrim( $this->mParser->recursiveTagParse( $parserInput ), "\n" );
764
765 if ( $wgCiteCacheReferences ) {
766 $serData = $this->mParser->serializeHalfParsedText( $ret );
767 $wgMemc->set( $cacheKey, $serData, 86400 );
768 }
769
770 } else {
771 $ret = $this->mParser->unserializeHalfParsedText( $data );
772 }
773
774 if ( $responsive ) {
775 // Use a DIV wrap because column-count on a list directly is broken in Chrome.
776 // See https://bugs.chromium.org/p/chromium/issues/detail?id=498730.
777 $wrapClasses = [ 'mw-references-wrap' ];
778 if ( count( $this->mRefs[$group] ) > 10 ) {
779 $wrapClasses[] = 'mw-references-columns';
780 }
781 $ret = Html::rawElement( 'div', [ 'class' => $wrapClasses ], $ret );
782 }
783
784 if ( !$this->mParser->getOptions()->getIsPreview() ) {
785 // save references data for later use by LinksUpdate hooks
786 $this->saveReferencesData( $group );
787 }
788
789 // done, clean up so we can reuse the group
790 unset( $this->mRefs[$group] );
791 unset( $this->mGroupCnt[$group] );
792
793 return $ret;
794 }
795
804 private function referencesFormatEntry( $key, $val ) {
805 // Anonymous reference
806 if ( !is_array( $val ) ) {
807 return wfMessage(
808 'cite_references_link_one',
809 self::getReferencesKey( $key ),
810 $this->refKey( $key ),
811 $this->referenceText( $key, $val )
812 )->inContentLanguage()->plain();
813 }
814 $text = $this->referenceText( $key, $val['text'] );
815 if ( isset( $val['follow'] ) ) {
816 return wfMessage(
817 'cite_references_no_link',
818 self::getReferencesKey( $val['follow'] ),
819 $text
820 )->inContentLanguage()->plain();
821 }
822 if ( !isset( $val['count'] ) ) {
823 // this handles the case of section preview for list-defined references
824 return wfMessage( 'cite_references_link_many',
825 self::getReferencesKey( $key . "-" . ( isset( $val['key'] ) ? $val['key'] : '' ) ),
826 '',
827 $text
828 )->inContentLanguage()->plain();
829 }
830 if ( $val['count'] < 0 ) {
831 return wfMessage(
832 'cite_references_link_one',
833 self::getReferencesKey( $val['key'] ),
834 # $this->refKey( $val['key'], $val['count'] ),
835 $this->refKey( $val['key'] ),
836 $text
837 )->inContentLanguage()->plain();
838 // Standalone named reference, I want to format this like an
839 // anonymous reference because displaying "1. 1.1 Ref text" is
840 // overkill and users frequently use named references when they
841 // don't need them for convenience
842 }
843 if ( $val['count'] === 0 ) {
844 return wfMessage(
845 'cite_references_link_one',
846 self::getReferencesKey( $key . "-" . $val['key'] ),
847 # $this->refKey( $key, $val['count'] ),
848 $this->refKey( $key, $val['key'] . "-" . $val['count'] ),
849 $text
850 )->inContentLanguage()->plain();
851 // Named references with >1 occurrences
852 }
853 $links = [];
854 // for group handling, we have an extra key here.
855 for ( $i = 0; $i <= $val['count']; ++$i ) {
856 $links[] = wfMessage(
857 'cite_references_link_many_format',
858 $this->refKey( $key, $val['key'] . "-$i" ),
859 $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
861 )->inContentLanguage()->plain();
862 }
863
864 $list = $this->listToText( $links );
865
866 return wfMessage( 'cite_references_link_many',
867 self::getReferencesKey( $key . "-" . $val['key'] ),
868 $list,
869 $text
870 )->inContentLanguage()->plain();
871 }
872
879 private function referenceText( $key, $text ) {
880 if ( !isset( $text ) || $text === '' ) {
881 if ( $this->mParser->getOptions()->getIsSectionPreview() ) {
882 return $this->warning( 'cite_warning_sectionpreview_no_text', $key, 'noparse' );
883 }
884 return $this->error( 'cite_error_references_no_text', $key, 'noparse' );
885 }
886 return '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\n";
887 }
888
901 private function referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {
902 global $wgContLang;
903 $scope = strlen( $max );
904 $ret = $wgContLang->formatNum(
905 sprintf( "%s.%0{$scope}s", $base, $offset )
906 );
907 return $ret;
908 }
909
921 if ( !isset( $this->mBacklinkLabels ) ) {
922 $this->genBacklinkLabels();
923 }
924 if ( isset( $this->mBacklinkLabels[$offset] ) ) {
925 return $this->mBacklinkLabels[$offset];
926 } else {
927 // Feed me!
928 return $this->error( 'cite_error_references_no_backlink_label', null, 'noparse' );
929 }
930 }
931
944 private function getLinkLabel( $offset, $group, $label ) {
945 $message = "cite_link_label_group-$group";
946 if ( !isset( $this->mLinkLabels[$group] ) ) {
947 $this->genLinkLabels( $group, $message );
948 }
949 if ( $this->mLinkLabels[$group] === false ) {
950 // Use normal representation, ie. "$group 1", "$group 2"...
951 return $label;
952 }
953
954 if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) {
955 return $this->mLinkLabels[$group][$offset - 1];
956 } else {
957 // Feed me!
958 return $this->error( 'cite_error_no_link_label_group', [ $group, $message ], 'noparse' );
959 }
960 }
961
973 private function refKey( $key, $num = null ) {
974 $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text();
975 $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text();
976 if ( isset( $num ) ) {
977 $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num )
978 ->inContentLanguage()->plain();
979 }
980
981 return "$prefix$key$suffix";
982 }
983
994 public static function getReferencesKey( $key ) {
995 $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text();
996 $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text();
997
998 return "$prefix$key$suffix";
999 }
1000
1016 private function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) {
1017 global $wgContLang;
1018 $label = is_null( $label ) ? ++$this->mGroupCnt[$group] : $label;
1019
1020 return
1021 $this->mParser->recursiveTagParse(
1022 wfMessage(
1023 'cite_reference_link',
1024 $this->refKey( $key, $count ),
1025 self::getReferencesKey( $key . $subkey ),
1026 $this->getLinkLabel( $label, $group,
1027 ( ( $group === self::DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) )
1028 )->inContentLanguage()->plain()
1029 );
1030 }
1031
1044 private function listToText( $arr ) {
1045 $cnt = count( $arr );
1046
1047 $sep = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain();
1048 $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain();
1049
1050 if ( $cnt === 1 ) {
1051 // Enforce always returning a string
1052 return (string)$arr[0];
1053 } else {
1054 $t = array_slice( $arr, 0, $cnt - 1 );
1055 return implode( $sep, $t ) . $and . $arr[$cnt - 1];
1056 }
1057 }
1058
1064 private function genBacklinkLabels() {
1065 $text = wfMessage( 'cite_references_link_many_format_backlink_labels' )
1066 ->inContentLanguage()->plain();
1067 $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text );
1068 }
1069
1078 private function genLinkLabels( $group, $message ) {
1079 $text = false;
1080 $msg = wfMessage( $message )->inContentLanguage();
1081 if ( $msg->exists() ) {
1082 $text = $msg->plain();
1083 }
1084 $this->mLinkLabels[$group] = ( !$text ) ? false : preg_split( '#[\n\t ]#', $text );
1085 }
1086
1095 public function clearState( Parser &$parser ) {
1096 if ( $parser->extCite !== $this ) {
1097 return $parser->extCite->clearState( $parser );
1098 }
1099
1100 # Don't clear state when we're in the middle of parsing
1101 # a <ref> tag
1102 if ( $this->mInCite || $this->mInReferences ) {
1103 return true;
1104 }
1105
1106 $this->mGroupCnt = [];
1107 $this->mOutCnt = 0;
1108 $this->mCallCnt = 0;
1109 $this->mRefs = [];
1110 $this->mReferencesErrors = [];
1111 $this->mRefCallStack = [];
1112
1113 return true;
1114 }
1115
1123 public function cloneState( Parser $parser ) {
1124 if ( $parser->extCite !== $this ) {
1125 return $parser->extCite->cloneState( $parser );
1126 }
1127
1128 $parser->extCite = clone $this;
1129 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1130 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1131
1132 // Clear the state, making sure it will actually work.
1133 $parser->extCite->mInCite = false;
1134 $parser->extCite->mInReferences = false;
1135 $parser->extCite->clearState( $parser );
1136
1137 return true;
1138 }
1139
1154 public function checkRefsNoReferences( $afterParse, &$parser, &$text ) {
1155 global $wgCiteResponsiveReferences;
1156 if ( is_null( $parser->extCite ) ) {
1157 return true;
1158 }
1159 if ( $parser->extCite !== $this ) {
1160 return $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text );
1161 }
1162
1163 if ( $afterParse ) {
1164 $this->mHaveAfterParse = true;
1165 } elseif ( $this->mHaveAfterParse ) {
1166 return true;
1167 }
1168
1169 if ( !$parser->getOptions()->getIsPreview() ) {
1170 // save references data for later use by LinksUpdate hooks
1171 if ( $this->mRefs && isset( $this->mRefs[self::DEFAULT_GROUP] ) ) {
1172 $this->saveReferencesData();
1173 }
1174 $isSectionPreview = false;
1175 } else {
1176 $isSectionPreview = $parser->getOptions()->getIsSectionPreview();
1177 }
1178
1179 $s = '';
1180 foreach ( $this->mRefs as $group => $refs ) {
1181 if ( !$refs ) {
1182 continue;
1183 }
1184 if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
1185 $s .= $this->referencesFormat( $group, $wgCiteResponsiveReferences );
1186 } else {
1187 $s .= "\n<br />" .
1188 $this->error( 'cite_error_group_refs_without_references', htmlspecialchars( $group ) );
1189 }
1190 }
1191 if ( $isSectionPreview && $s !== '' ) {
1192 // provide a preview of references in its own section
1193 $text .= "\n" . '<div class="mw-ext-cite-cite_section_preview_references" >';
1194 $headerMsg = wfMessage( 'cite_section_preview_references' );
1195 if ( !$headerMsg->isDisabled() ) {
1196 $text .= '<h2 id="mw-ext-cite-cite_section_preview_references_header" >'
1197 . $headerMsg->escaped()
1198 . '</h2>';
1199 }
1200 $text .= $s . '</div>';
1201 } else {
1202 $text .= $s;
1203 }
1204 return true;
1205 }
1206
1214 private function saveReferencesData( $group = self::DEFAULT_GROUP ) {
1215 global $wgCiteStoreReferencesData;
1216 if ( !$wgCiteStoreReferencesData ) {
1217 return;
1218 }
1219 $savedRefs = $this->mParser->getOutput()->getExtensionData( self::EXT_DATA_KEY );
1220 if ( $savedRefs === null ) {
1221 // Initialize array structure
1222 $savedRefs = [
1223 'refs' => [],
1224 'version' => self::DATA_VERSION_NUMBER,
1225 ];
1226 }
1227 if ( $this->mBumpRefData ) {
1228 // This handles pages with multiple <references/> tags with <ref> tags in between.
1229 // On those, a group can appear several times, so we need to avoid overwriting
1230 // a previous appearance.
1231 $savedRefs['refs'][] = [];
1232 $this->mBumpRefData = false;
1233 }
1234 $n = count( $savedRefs['refs'] ) - 1;
1235 // save group
1236 $savedRefs['refs'][$n][$group] = $this->mRefs[$group];
1237
1238 $this->mParser->getOutput()->setExtensionData( self::EXT_DATA_KEY, $savedRefs );
1239 }
1240
1250 public function checkAnyCalls( &$output ) {
1251 global $wgParser;
1252 /* InlineEditor always uses $wgParser */
1253 return ( $wgParser->extCite->mCallCnt <= 0 );
1254 }
1255
1263 public static function setHooks( Parser $parser ) {
1264 global $wgHooks;
1265
1266 $parser->extCite = new self();
1267
1268 if ( !self::$hooksInstalled ) {
1269 $wgHooks['ParserClearState'][] = [ $parser->extCite, 'clearState' ];
1270 $wgHooks['ParserCloned'][] = [ $parser->extCite, 'cloneState' ];
1271 $wgHooks['ParserAfterParse'][] = [ $parser->extCite, 'checkRefsNoReferences', true ];
1272 $wgHooks['ParserBeforeTidy'][] = [ $parser->extCite, 'checkRefsNoReferences', false ];
1273 $wgHooks['InlineEditorPartialAfterParse'][] = [ $parser->extCite, 'checkAnyCalls' ];
1274 self::$hooksInstalled = true;
1275 }
1276 $parser->setHook( 'ref', [ $parser->extCite, 'ref' ] );
1277 $parser->setHook( 'references', [ $parser->extCite, 'references' ] );
1278
1279 return true;
1280 }
1281
1290 private function error( $key, $param = null, $parse = 'parse' ) {
1291 # For ease of debugging and because errors are rare, we
1292 # use the user language and split the parser cache.
1293 $lang = $this->mParser->getOptions()->getUserLangObj();
1294 $dir = $lang->getDir();
1295
1296 # We rely on the fact that PHP is okay with passing unused argu-
1297 # ments to functions. If $1 is not used in the message, wfMessage will
1298 # just ignore the extra parameter.
1299 $msg = wfMessage(
1300 'cite_error',
1301 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1302 )
1303 ->inLanguage( $lang )
1304 ->plain();
1305
1306 $this->mParser->addTrackingCategory( 'cite-tracking-category-cite-error' );
1307
1308 $ret = Html::rawElement(
1309 'span',
1310 [
1311 'class' => 'error mw-ext-cite-error',
1312 'lang' => $lang->getHtmlCode(),
1313 'dir' => $dir,
1314 ],
1315 $msg
1316 );
1317
1318 if ( $parse === 'parse' ) {
1319 $ret = $this->mParser->recursiveTagParse( $ret );
1320 }
1321
1322 return $ret;
1323 }
1324
1333 private function warning( $key, $param = null, $parse = 'parse' ) {
1334 # For ease of debugging and because errors are rare, we
1335 # use the user language and split the parser cache.
1336 $lang = $this->mParser->getOptions()->getUserLangObj();
1337 $dir = $lang->getDir();
1338
1339 # We rely on the fact that PHP is okay with passing unused argu-
1340 # ments to functions. If $1 is not used in the message, wfMessage will
1341 # just ignore the extra parameter.
1342 $msg = wfMessage(
1343 'cite_warning',
1344 wfMessage( $key, $param )->inLanguage( $lang )->plain()
1345 )
1346 ->inLanguage( $lang )
1347 ->plain();
1348
1349 $key = preg_replace( '/^cite_warning_/', '', $key ) . '';
1350 $ret = Html::rawElement(
1351 'span',
1352 [
1353 'class' => 'warning mw-ext-cite-warning mw-ext-cite-warning-' .
1354 Sanitizer::escapeClass( $key ),
1355 'lang' => $lang->getHtmlCode(),
1356 'dir' => $dir,
1357 ],
1358 $msg
1359 );
1360
1361 if ( $parse === 'parse' ) {
1362 $ret = $this->mParser->recursiveTagParse( $ret );
1363 }
1364
1365 return $ret;
1366 }
1367
1375 public static function getStoredReferences( Title $title ) {
1376 global $wgCiteStoreReferencesData;
1377 if ( !$wgCiteStoreReferencesData ) {
1378 return false;
1379 }
1380 $cache = ObjectCache::getMainWANInstance();
1381 $key = $cache->makeKey( self::EXT_DATA_KEY, $title->getArticleID() );
1382 return $cache->getWithSetCallback(
1383 $key,
1384 self::CACHE_DURATION_ONFETCH,
1385 function ( $oldValue, &$ttl, array &$setOpts ) use ( $title ) {
1386 $dbr = wfGetDB( DB_REPLICA );
1387 $setOpts += Database::getCacheSetOptions( $dbr );
1388 return self::recursiveFetchRefsFromDB( $title, $dbr );
1389 },
1390 [
1391 'checkKeys' => [ $key ],
1392 'lockTSE' => 30,
1393 ]
1394 );
1395 }
1396
1408 private static function recursiveFetchRefsFromDB( Title $title, DatabaseBase $dbr,
1409 $string = '', $i = 1 ) {
1410 $id = $title->getArticleID();
1411 $result = $dbr->selectField(
1412 'page_props',
1413 'pp_value',
1414 [
1415 'pp_page' => $id,
1416 'pp_propname' => 'references-' . $i
1417 ],
1418 __METHOD__
1419 );
1420 if ( $result !== false ) {
1421 $string .= $result;
1422 $decodedString = gzdecode( $string );
1423 if ( $decodedString !== false ) {
1424 $json = json_decode( $decodedString, true );
1425 if ( json_last_error() === JSON_ERROR_NONE ) {
1426 return $json;
1427 }
1428 // corrupted json ?
1429 // shouldn't happen since when string is truncated, gzdecode should fail
1430 wfDebug( "Corrupted json detected when retrieving stored references for title id $id" );
1431 }
1432 // if gzdecode fails, try to fetch next references- property value
1433 return self::recursiveFetchRefsFromDB( $title, $dbr, $string, ++$i );
1434
1435 } else {
1436 // no refs stored in page_props at this index
1437 if ( $i > 1 ) {
1438 // shouldn't happen
1439 wfDebug( "Failed to retrieve stored references for title id $id" );
1440 }
1441 return false;
1442 }
1443 }
1444
1445}
serialize()
$dir
Definition Autoload.php:8
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.
wfMemcKey()
Make a cache key for the local wiki.
$wgParser
Definition Setup.php:832
global $argv
A parser extension that adds two tags, <ref> and <references> for adding citations to pages.
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:944
guardedReferences( $str, array $argv, Parser $parser, $group=self::DEFAULT_GROUP)
Definition Cite.php:642
array $mLinkLabels
The links to use per group, in order.
Definition Cite.php:129
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:390
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:1016
static recursiveFetchRefsFromDB(Title $title, DatabaseBase $dbr, $string='', $i=1)
Reconstructs compressed json by successively retrieving the properties references-1,...
Definition Cite.php:1408
referenceText( $key, $text)
Returns formatted reference text.
Definition Cite.php:879
error( $key, $param=null, $parse='parse')
Return an error message based on an error ID.
Definition Cite.php:1290
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:1078
genBacklinkLabels()
Generate the labels to pass to the 'cite_references_link_many_format' message, the format is an arbit...
Definition Cite.php:1064
references( $str, array $argv, Parser $parser, PPFrame $frame)
Callback function for <references>
Definition Cite.php:617
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
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:1154
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:1214
guardedRef( $str, array $argv, Parser $parser, $default_group=self::DEFAULT_GROUP)
Definition Cite.php:240
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:1095
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:1044
string[] $mBacklinkLabels
The backlinks, in order, to pass as $3 to 'cite_references_link_many_format', defined in 'cite_refere...
Definition Cite.php:122
array $mRefCallStack
<ref> call stack Used to cleanup out of sequence ref calls created by #tag See description of functio...
Definition Cite.php:181
static getStoredReferences(Title $title)
Fetch references stored for the given title in page_props For performance, results are cached.
Definition Cite.php:1375
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:973
const CACHE_DURATION_ONPARSE
Cache duration set when parsing a page with references.
Definition Cite.php:55
boolean $mHaveAfterParse
True when the ParserAfterParse hook has been called.
Definition Cite.php:142
cloneState(Parser $parser)
Gets run when the parser is cloned.
Definition Cite.php:1123
referencesFormatEntryAlternateBacklinkLabel( $offset)
Generate a custom format backlink given an offset, e.g.
Definition Cite.php:920
referencesFormat( $group, $responsive)
Make output to be returned from the references() function.
Definition Cite.php:733
int $mOutCnt
Count for user displayed output (ref[1], ref[2], ...)
Definition Cite.php:100
int[] $mGroupCnt
Definition Cite.php:105
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:901
static setHooks(Parser $parser)
Initialize the parser hooks.
Definition Cite.php:1263
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:994
boolean $mInReferences
True when a <references> tag is being processed.
Definition Cite.php:158
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:804
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:562
checkAnyCalls(&$output)
Hook for the InlineEditor extension.
Definition Cite.php:1250
warning( $key, $param=null, $parse='parse')
Return a warning message based on a warning ID.
Definition Cite.php:1333
stack( $str, $key=null, $group, $follow, array $call)
Populate $this->mRefs based on input and arguments to <ref>
Definition Cite.php:447
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:70
Represents a title within MediaWiki.
Definition Title.php:39
if(! $regexes) $dbr
Definition cleanup.php:94
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
namespace being checked & $result
Definition hooks.txt:2293
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2572
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2225
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:2581
either a plain
Definition hooks.txt:2026
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:1976
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:1975
$wgHooks['ArticleShow'][]
Definition hooks.txt:108
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php For a description of the see design txt $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest to get request data $wgMemc
Definition globals.txt:66
setVolatile( $flag=true)
Set the "volatile" flag.
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang