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