]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - extensions/Cite/includes/Cite.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / extensions / Cite / includes / Cite.php
1 <?php
2
3 /**
4  * A parser extension that adds two tags, <ref> and <references> for adding
5  * citations to pages
6  *
7  * @ingroup Extensions
8  *
9  * Documentation
10  * @link http://www.mediawiki.org/wiki/Extension:Cite/Cite.php
11  *
12  * <cite> definition in HTML
13  * @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE
14  *
15  * <cite> definition in XHTML 2.0
16  * @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite
17  *
18  * @bug https://phabricator.wikimedia.org/T6579
19  *
20  * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
21  * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
22  * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
23  */
24
25 /**
26  * WARNING: MediaWiki core hardcodes this class name to check if the
27  * Cite extension is installed. See T89151.
28  */
29 class Cite {
30
31         /**
32          * @todo document
33          */
34         const DEFAULT_GROUP = '';
35
36         /**
37          * Maximum storage capacity for pp_value field of page_props table
38          * @todo Find a way to retrieve this information from the DBAL
39          */
40         const MAX_STORAGE_LENGTH = 65535; // Size of MySQL 'blob' field
41
42         /**
43          * Key used for storage in parser output's ExtensionData and ObjectCache
44          */
45         const EXT_DATA_KEY = 'Cite:References';
46
47         /**
48          * Version number in case we change the data structure in the future
49          */
50         const DATA_VERSION_NUMBER = 1;
51
52         /**
53          * Cache duration set when parsing a page with references
54          */
55         const CACHE_DURATION_ONPARSE = 3600; // 1 hour
56
57         /**
58          * Cache duration set when fetching references from db
59          */
60         const CACHE_DURATION_ONFETCH = 18000; // 5 hours
61
62         /**
63          * Datastructure representing <ref> input, in the format of:
64          * <code>
65          * [
66          *      'user supplied' => [
67          *              'text' => 'user supplied reference & key',
68          *              'count' => 1, // occurs twice
69          *              'number' => 1, // The first reference, we want
70          *                             // all occourances of it to
71          *                             // use the same number
72          *      ],
73          *      0 => 'Anonymous reference',
74          *      1 => 'Another anonymous reference',
75          *      'some key' => [
76          *              'text' => 'this one occurs once'
77          *              'count' => 0,
78          *              'number' => 4
79          *      ],
80          *      3 => 'more stuff'
81          * ];
82          * </code>
83          *
84          * This works because:
85          * * PHP's datastructures are guaranteed to be returned in the
86          *   order that things are inserted into them (unless you mess
87          *   with that)
88          * * User supplied keys can't be integers, therefore avoiding
89          *   conflict with anonymous keys
90          *
91          * @var array[]
92          */
93         private $mRefs = [];
94
95         /**
96          * Count for user displayed output (ref[1], ref[2], ...)
97          *
98          * @var int
99          */
100         private $mOutCnt = 0;
101
102         /**
103          * @var int[]
104          */
105         private $mGroupCnt = [];
106
107         /**
108          * Counter to track the total number of (useful) calls to either the
109          * ref or references tag hook
110          *
111          * @var int
112          */
113         private $mCallCnt = 0;
114
115         /**
116          * The backlinks, in order, to pass as $3 to
117          * 'cite_references_link_many_format', defined in
118          * 'cite_references_link_many_format_backlink_labels
119          *
120          * @var string[]
121          */
122         private $mBacklinkLabels;
123
124         /**
125          * The links to use per group, in order.
126          *
127          * @var array
128          */
129         private $mLinkLabels = [];
130
131         /**
132          * @var Parser
133          */
134         private $mParser;
135
136         /**
137          * True when the ParserAfterParse hook has been called.
138          * Used to avoid doing anything in ParserBeforeTidy.
139          *
140          * @var boolean
141          */
142         private $mHaveAfterParse = false;
143
144         /**
145          * True when a <ref> tag is being processed.
146          * Used to avoid infinite recursion
147          *
148          * @var boolean
149          */
150         public $mInCite = false;
151
152         /**
153          * True when a <references> tag is being processed.
154          * Used to detect the use of <references> to define refs
155          *
156          * @var boolean
157          */
158         public $mInReferences = false;
159
160         /**
161          * Error stack used when defining refs in <references>
162          *
163          * @var string[]
164          */
165         private $mReferencesErrors = [];
166
167         /**
168          * Group used when in <references> block
169          *
170          * @var string
171          */
172         private $mReferencesGroup = '';
173
174         /**
175          * <ref> call stack
176          * Used to cleanup out of sequence ref calls created by #tag
177          * See description of function rollbackRef.
178          *
179          * @var array
180          */
181         private $mRefCallStack = [];
182
183         /**
184          * @var bool
185          */
186         private $mBumpRefData = false;
187
188         /**
189          * Did we install us into $wgHooks yet?
190          * @var Boolean
191          */
192         private static $hooksInstalled = false;
193
194         /**
195          * Callback function for <ref>
196          *
197          * @param string|null $str Raw content of the <ref> tag.
198          * @param string[] $argv Arguments
199          * @param Parser $parser
200          * @param PPFrame $frame
201          *
202          * @return string
203          */
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
231         /**
232          * @param string|null $str Raw content of the <ref> tag.
233          * @param string[] $argv Arguments
234          * @param Parser $parser
235          * @param string $default_group
236          *
237          * @throws Exception
238          * @return string
239          */
240         private function guardedRef(
241                 $str,
242                 array $argv,
243                 Parser $parser,
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 ) {
254                                 $group = $this->mReferencesGroup;
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
378         /**
379          * Parse the arguments to the <ref> tag
380          *
381          *  "name" : Key of the reference.
382          *  "group" : Group to which it belongs. Needs to be passed to <references /> too.
383          *  "follow" : If the current reference is the continuation of another, key of that reference.
384          *
385          *
386          * @param string[] $argv The argument vector
387          * @return mixed false on invalid input, a string on valid
388          *               input and null on no input
389          */
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
435         /**
436          * Populate $this->mRefs based on input and arguments to <ref>
437          *
438          * @param string $str Input from the <ref> tag
439          * @param string|null $key Argument to the <ref> tag as returned by $this->refArg()
440          * @param string $group
441          * @param string|null $follow
442          * @param string[] $call
443          *
444          * @throws Exception
445          * @return string
446          */
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
541         /**
542          * Partially undoes the effect of calls to stack()
543          *
544          * Called by guardedReferences()
545          *
546          * The option to define <ref> within <references> makes the
547          * behavior of <ref> context dependent.  This is normally fine
548          * but certain operations (especially #tag) lead to out-of-order
549          * parser evaluation with the <ref> tags being processed before
550          * their containing <reference> element is read.  This leads to
551          * stack corruption that this function works to fix.
552          *
553          * This function is not a total rollback since some internal
554          * counters remain incremented.  Doing so prevents accidentally
555          * corrupting certain links.
556          *
557          * @param string $type
558          * @param string|null $key
559          * @param string $group
560          * @param int $index
561          */
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
607         /**
608          * Callback function for <references>
609          *
610          * @param string|null $str Raw content of the <references> tag.
611          * @param string[] $argv Arguments
612          * @param Parser $parser
613          * @param PPFrame $frame
614          *
615          * @return string
616          */
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
634         /**
635          * @param string|null $str Raw content of the <references> tag.
636          * @param string[] $argv
637          * @param Parser $parser
638          * @param string $group
639          *
640          * @return string
641          */
642         private function guardedReferences(
643                 $str,
644                 array $argv,
645                 Parser $parser,
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
726         /**
727          * Make output to be returned from the references() function
728          *
729          * @param string $group
730          * @param bool $responsive
731          * @return string HTML ready for output
732          */
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
796         /**
797          * Format a single entry for the referencesFormat() function
798          *
799          * @param string $key The key of the reference
800          * @param mixed $val The value of the reference, string for anonymous
801          *                   references, array for user-suppplied
802          * @return string Wikitext
803          */
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'] ),
860                                         $this->referencesFormatEntryAlternateBacklinkLabel( $i )
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
873         /**
874          * Returns formatted reference text
875          * @param String $key
876          * @param String $text
877          * @return String
878          */
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
889         /**
890          * Generate a numeric backlink given a base number and an
891          * offset, e.g. $base = 1, $offset = 2; = 1.2
892          * Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100
893          *
894          * @static
895          *
896          * @param int $base The base
897          * @param int $offset The offset
898          * @param int $max Maximum value expected.
899          * @return string
900          */
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
910         /**
911          * Generate a custom format backlink given an offset, e.g.
912          * $offset = 2; = c if $this->mBacklinkLabels = [ 'a',
913          * 'b', 'c', ...]. Return an error if the offset > the # of
914          * array items
915          *
916          * @param int $offset The offset
917          *
918          * @return string
919          */
920         private function referencesFormatEntryAlternateBacklinkLabel( $offset ) {
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
932         /**
933          * Generate a custom format link for a group given an offset, e.g.
934          * the second <ref group="foo"> is b if $this->mLinkLabels["foo"] =
935          * [ 'a', 'b', 'c', ...].
936          * Return an error if the offset > the # of array items
937          *
938          * @param int $offset The offset
939          * @param string $group The group name
940          * @param string $label The text to use if there's no message for them.
941          *
942          * @return string
943          */
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
962         /**
963          * Return an id for use in wikitext output based on a key and
964          * optionally the number of it, used in <references>, not <ref>
965          * (since otherwise it would link to itself)
966          *
967          * @static
968          *
969          * @param string $key The key
970          * @param int $num The number of the key
971          * @return string A key for use in wikitext
972          */
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
984         /**
985          * Return an id for use in wikitext output based on a key and
986          * optionally the number of it, used in <ref>, not <references>
987          * (since otherwise it would link to itself)
988          *
989          * @static
990          *
991          * @param string $key The key
992          * @return string A key for use in wikitext
993          */
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
1001         /**
1002          * Generate a link (<sup ...) for the <ref> element from a key
1003          * and return XHTML ready for output
1004          *
1005          * @param string $group
1006          * @param string $key The key for the link
1007          * @param int $count The index of the key, used for distinguishing
1008          *                   multiple occurrences of the same key
1009          * @param int $label The label to use for the link, I want to
1010          *                   use the same label for all occourances of
1011          *                   the same named reference.
1012          * @param string $subkey
1013          *
1014          * @return string
1015          */
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
1032         /**
1033          * This does approximately the same thing as
1034          * Language::listToText() but due to this being used for a
1035          * slightly different purpose (people might not want , as the
1036          * first separator and not 'and' as the second, and this has to
1037          * use messages from the content language) I'm rolling my own.
1038          *
1039          * @static
1040          *
1041          * @param array $arr The array to format
1042          * @return string
1043          */
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
1059         /**
1060          * Generate the labels to pass to the
1061          * 'cite_references_link_many_format' message, the format is an
1062          * arbitrary number of tokens separated by [\t\n ]
1063          */
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
1070         /**
1071          * Generate the labels to pass to the
1072          * 'cite_reference_link' message instead of numbers, the format is an
1073          * arbitrary number of tokens separated by [\t\n ]
1074          *
1075          * @param string $group
1076          * @param string $message
1077          */
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
1087         /**
1088          * Gets run when Parser::clearState() gets run, since we don't
1089          * want the counts to transcend pages and other instances
1090          *
1091          * @param Parser $parser
1092          *
1093          * @return bool
1094          */
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
1116         /**
1117          * Gets run when the parser is cloned.
1118          *
1119          * @param Parser $parser
1120          *
1121          * @return bool
1122          */
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
1140         /**
1141          * Called at the end of page processing to append a default references
1142          * section, if refs were used without a main references tag. If there are references
1143          * in a custom group, and there is no references tag for it, show an error
1144          * message for that group.
1145          * If we are processing a section preview, this adds the missing
1146          * references tags and does not add the errors.
1147          *
1148          * @param bool $afterParse True if called from the ParserAfterParse hook
1149          * @param Parser $parser
1150          * @param string $text
1151          *
1152          * @return bool
1153          */
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
1207         /**
1208          * Saves references in parser extension data
1209          * This is called by each <references/> tag, and by checkRefsNoReferences
1210          * Assumes $this->mRefs[$group] is set
1211          *
1212          * @param $group
1213          */
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
1241         /**
1242          * Hook for the InlineEditor extension.
1243          * If any ref or reference reference tag is in the text,
1244          * the entire page should be reparsed, so we return false in that case.
1245          *
1246          * @param $output
1247          *
1248          * @return bool
1249          */
1250         public function checkAnyCalls( &$output ) {
1251                 global $wgParser;
1252                 /* InlineEditor always uses $wgParser */
1253                 return ( $wgParser->extCite->mCallCnt <= 0 );
1254         }
1255
1256         /**
1257          * Initialize the parser hooks
1258          *
1259          * @param Parser $parser
1260          *
1261          * @return bool
1262          */
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
1282         /**
1283          * Return an error message based on an error ID
1284          *
1285          * @param string $key   Message name for the error
1286          * @param string|null $param Parameter to pass to the message
1287          * @param string $parse Whether to parse the message ('parse') or not ('noparse')
1288          * @return string XHTML or wikitext ready for output
1289          */
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
1325         /**
1326          * Return a warning message based on a warning ID
1327          *
1328          * @param string $key   Message name for the warning. Name should start with cite_warning_
1329          * @param string|null $param Parameter to pass to the message
1330          * @param string $parse Whether to parse the message ('parse') or not ('noparse')
1331          * @return string XHTML or wikitext ready for output
1332          */
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
1368         /**
1369          * Fetch references stored for the given title in page_props
1370          * For performance, results are cached
1371          *
1372          * @param Title $title
1373          * @return array|false
1374          */
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
1397         /**
1398          * Reconstructs compressed json by successively retrieving the properties references-1, -2, etc
1399          * It attempts the next step when a decoding error occurs.
1400          * Returns json_decoded uncompressed string, with validation of json
1401          *
1402          * @param Title $title
1403          * @param DatabaseBase $dbr
1404          * @param string $string
1405          * @param int $i
1406          * @return array|false
1407          */
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 }