2008-06-06 20:38:04 +00:00
|
|
|
<?php
|
|
|
|
|
2016-09-21 11:01:25 +00:00
|
|
|
/**
|
2008-06-06 20:38:04 +00:00
|
|
|
* A parser extension that adds two tags, <ref> and <references> for adding
|
|
|
|
* citations to pages
|
|
|
|
*
|
2010-06-06 15:12:22 +00:00
|
|
|
* @ingroup Extensions
|
2008-06-06 20:38:04 +00:00
|
|
|
*
|
2016-09-21 11:01:25 +00:00
|
|
|
* Documentation
|
2018-10-30 12:08:46 +00:00
|
|
|
* @link https://www.mediawiki.org/wiki/Extension:Cite/Cite.php
|
2016-09-21 11:01:25 +00:00
|
|
|
*
|
|
|
|
* <cite> definition in HTML
|
|
|
|
* @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE
|
|
|
|
*
|
|
|
|
* <cite> definition in XHTML 2.0
|
|
|
|
* @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite
|
2008-06-06 20:38:04 +00:00
|
|
|
*
|
2016-09-29 14:19:16 +00:00
|
|
|
* @bug https://phabricator.wikimedia.org/T6579
|
2008-06-06 20:38:04 +00:00
|
|
|
*
|
|
|
|
* @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
|
|
|
|
* @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
|
2017-12-29 12:12:35 +00:00
|
|
|
* @license GPL-2.0-or-later
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
|
|
|
|
2019-11-19 14:12:11 +00:00
|
|
|
namespace Cite;
|
|
|
|
|
2019-12-09 14:07:37 +00:00
|
|
|
use Html;
|
2019-12-20 18:31:54 +00:00
|
|
|
use LogicException;
|
2019-11-19 14:12:11 +00:00
|
|
|
use Parser;
|
|
|
|
use Sanitizer;
|
2019-11-22 21:45:11 +00:00
|
|
|
use StatusValue;
|
2017-12-22 20:28:29 +00:00
|
|
|
|
2008-08-08 19:11:31 +00:00
|
|
|
class Cite {
|
2015-04-30 18:34:47 +00:00
|
|
|
|
2019-11-28 14:39:11 +00:00
|
|
|
public const DEFAULT_GROUP = '';
|
2015-04-30 18:34:47 +00:00
|
|
|
|
2019-10-24 09:23:44 +00:00
|
|
|
/**
|
|
|
|
* Wikitext attribute name for Book Referencing.
|
|
|
|
*/
|
2019-11-12 10:03:44 +00:00
|
|
|
public const BOOK_REF_ATTRIBUTE = 'extends';
|
2019-10-24 09:23:44 +00:00
|
|
|
|
2019-11-08 11:59:09 +00:00
|
|
|
/**
|
|
|
|
* Page property key for the Book Referencing `extends` attribute.
|
|
|
|
*/
|
|
|
|
public const BOOK_REF_PROPERTY = 'ref-extends';
|
|
|
|
|
2019-11-22 22:17:23 +00:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $isPagePreview;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $isSectionPreview;
|
|
|
|
|
2019-11-28 14:39:11 +00:00
|
|
|
/**
|
|
|
|
* @var FootnoteMarkFormatter
|
|
|
|
*/
|
|
|
|
private $footnoteMarkFormatter;
|
|
|
|
|
2019-11-28 14:39:58 +00:00
|
|
|
/**
|
2019-12-09 15:36:34 +00:00
|
|
|
* @var ReferencesFormatter
|
2019-11-28 14:39:58 +00:00
|
|
|
*/
|
2019-12-09 15:36:34 +00:00
|
|
|
private $referencesFormatter;
|
2019-11-28 14:39:58 +00:00
|
|
|
|
2019-11-19 10:29:33 +00:00
|
|
|
/**
|
2019-12-09 16:08:57 +00:00
|
|
|
* @var ErrorReporter
|
2019-11-19 10:29:33 +00:00
|
|
|
*/
|
|
|
|
private $errorReporter;
|
|
|
|
|
2008-06-06 20:38:04 +00:00
|
|
|
/**
|
2009-07-26 22:15:13 +00:00
|
|
|
* True when a <ref> tag is being processed.
|
2008-06-06 20:38:04 +00:00
|
|
|
* Used to avoid infinite recursion
|
2011-02-22 00:07:21 +00:00
|
|
|
*
|
2019-10-25 08:34:35 +00:00
|
|
|
* @var bool
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
2019-11-19 11:01:10 +00:00
|
|
|
private $mInCite = false;
|
2009-07-26 22:15:13 +00:00
|
|
|
|
|
|
|
/**
|
2019-11-19 13:34:19 +00:00
|
|
|
* @var null|string The current group name while parsing nested <ref> in <references>. Null when
|
|
|
|
* parsing <ref> outside of <references>. Warning, an empty string is a valid group name!
|
2009-07-26 22:15:13 +00:00
|
|
|
*/
|
2019-11-19 13:34:19 +00:00
|
|
|
private $inReferencesGroup = null;
|
2009-07-26 22:15:13 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Error stack used when defining refs in <references>
|
2011-02-22 00:07:21 +00:00
|
|
|
*
|
2016-09-21 11:01:25 +00:00
|
|
|
* @var string[]
|
2009-07-26 22:15:13 +00:00
|
|
|
*/
|
2016-09-21 11:01:25 +00:00
|
|
|
private $mReferencesErrors = [];
|
2009-07-26 22:15:13 +00:00
|
|
|
|
2015-06-08 14:05:06 +00:00
|
|
|
/**
|
2019-11-29 13:37:31 +00:00
|
|
|
* @var ReferenceStack
|
2015-06-08 14:05:06 +00:00
|
|
|
*/
|
2019-11-22 17:28:51 +00:00
|
|
|
private $referenceStack;
|
2015-06-08 14:05:06 +00:00
|
|
|
|
2019-11-19 10:29:33 +00:00
|
|
|
/**
|
|
|
|
* @param Parser $parser
|
|
|
|
*/
|
2019-12-11 13:58:25 +00:00
|
|
|
public function __construct( Parser $parser ) {
|
|
|
|
$this->isPagePreview = $parser->getOptions()->getIsPreview();
|
|
|
|
$this->isSectionPreview = $parser->getOptions()->getIsSectionPreview();
|
|
|
|
$messageLocalizer = new ReferenceMessageLocalizer( $parser->getContentLanguage() );
|
2019-12-11 15:05:19 +00:00
|
|
|
$this->errorReporter = new ErrorReporter( $messageLocalizer );
|
2019-12-11 13:58:25 +00:00
|
|
|
$this->referenceStack = new ReferenceStack( $this->errorReporter );
|
|
|
|
$anchorFormatter = new AnchorFormatter( $messageLocalizer );
|
|
|
|
$this->footnoteMarkFormatter = new FootnoteMarkFormatter(
|
2019-12-09 14:07:37 +00:00
|
|
|
$this->errorReporter,
|
|
|
|
$anchorFormatter,
|
|
|
|
$messageLocalizer
|
|
|
|
);
|
2019-12-11 13:58:25 +00:00
|
|
|
$this->referencesFormatter = new ReferencesFormatter(
|
2019-12-09 14:07:37 +00:00
|
|
|
$this->errorReporter,
|
|
|
|
$anchorFormatter,
|
|
|
|
$messageLocalizer
|
|
|
|
);
|
2019-11-19 10:29:33 +00:00
|
|
|
}
|
|
|
|
|
2008-06-06 20:38:04 +00:00
|
|
|
/**
|
2015-06-08 14:05:06 +00:00
|
|
|
* Callback function for <ref>
|
2008-06-06 20:38:04 +00:00
|
|
|
*
|
2015-10-26 14:44:02 +00:00
|
|
|
* @param Parser $parser
|
2019-12-19 13:20:10 +00:00
|
|
|
* @param ?string $text Raw, untrimmed wikitext content of the <ref> tag, if any
|
|
|
|
* @param string[] $argv Arguments as given in <ref name=…>, already trimmed
|
2011-05-28 20:44:24 +00:00
|
|
|
*
|
2019-11-12 12:06:39 +00:00
|
|
|
* @return string|false False in case a <ref> tag is not allowed in the current context
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
2019-12-19 13:20:10 +00:00
|
|
|
public function ref( Parser $parser, ?string $text, array $argv ) {
|
2015-06-08 14:05:06 +00:00
|
|
|
if ( $this->mInCite ) {
|
2019-11-12 12:06:39 +00:00
|
|
|
return false;
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
2014-10-01 14:25:58 +00:00
|
|
|
|
2019-11-19 10:29:33 +00:00
|
|
|
$this->mInCite = true;
|
2019-12-19 09:16:14 +00:00
|
|
|
$ret = $this->guardedRef( $parser, $text, $argv );
|
2014-10-01 14:25:58 +00:00
|
|
|
$this->mInCite = false;
|
|
|
|
|
2015-06-08 14:05:06 +00:00
|
|
|
return $ret;
|
2014-12-19 18:38:37 +00:00
|
|
|
}
|
|
|
|
|
2019-11-22 21:45:11 +00:00
|
|
|
/**
|
2019-12-19 09:16:14 +00:00
|
|
|
* @param ?string $text
|
2019-12-21 01:25:58 +00:00
|
|
|
* @param string $group
|
2019-12-19 09:16:14 +00:00
|
|
|
* @param ?string $name
|
|
|
|
* @param ?string $extends
|
|
|
|
* @param ?string $follow
|
|
|
|
* @param ?string $dir
|
2020-01-09 10:50:32 +00:00
|
|
|
*
|
2019-11-22 21:45:11 +00:00
|
|
|
* @return StatusValue
|
|
|
|
*/
|
2019-11-28 10:15:19 +00:00
|
|
|
private function validateRef(
|
|
|
|
?string $text,
|
2019-12-21 01:25:58 +00:00
|
|
|
string $group,
|
2019-12-19 09:16:14 +00:00
|
|
|
?string $name,
|
2019-12-16 14:33:55 +00:00
|
|
|
?string $extends,
|
2019-12-19 09:16:14 +00:00
|
|
|
?string $follow,
|
2019-12-16 14:33:55 +00:00
|
|
|
?string $dir
|
2019-11-28 10:15:19 +00:00
|
|
|
) : StatusValue {
|
2019-12-21 01:25:58 +00:00
|
|
|
if ( ctype_digit( (string)$name )
|
|
|
|
|| ctype_digit( (string)$extends )
|
|
|
|
|| ctype_digit( (string)$follow )
|
|
|
|
) {
|
2019-11-26 09:06:15 +00:00
|
|
|
// Numeric names mess up the resulting id's, potentially producing
|
|
|
|
// duplicate id's in the XHTML. The Right Thing To Do
|
|
|
|
// would be to mangle them, but it's not really high-priority
|
|
|
|
// (and would produce weird id's anyway).
|
|
|
|
return StatusValue::newFatal( 'cite_error_ref_numeric_key' );
|
|
|
|
}
|
|
|
|
|
2019-12-09 08:37:38 +00:00
|
|
|
if ( $extends ) {
|
|
|
|
// Temporary feature flag until mainstreamed, see T236255
|
|
|
|
global $wgCiteBookReferencing;
|
|
|
|
if ( !$wgCiteBookReferencing ) {
|
|
|
|
return StatusValue::newFatal( 'cite_error_ref_too_many_keys' );
|
|
|
|
}
|
|
|
|
|
|
|
|
$groupRefs = $this->referenceStack->getGroupRefs( $group );
|
2020-01-16 13:57:51 +00:00
|
|
|
if ( isset( $groupRefs[$name] ) && !isset( $groupRefs[$name]['extends'] ) ) {
|
|
|
|
// T242141: A top-level <ref> can't be changed into a sub-reference
|
|
|
|
return StatusValue::newFatal( 'cite_error_references_duplicate_key', $name );
|
|
|
|
} elseif ( isset( $groupRefs[$extends]['extends'] ) ) {
|
|
|
|
// A sub-reference can not be extended a second time (no nesting)
|
2019-12-09 15:36:34 +00:00
|
|
|
// TODO: Introduce a specific error for this case, reuse in formatReferences()!
|
2019-12-09 08:37:38 +00:00
|
|
|
return StatusValue::newFatal( 'cite_error_ref_too_many_keys' );
|
|
|
|
}
|
2019-11-25 15:31:56 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 15:38:35 +00:00
|
|
|
if ( $follow && ( $name || $extends ) ) {
|
|
|
|
// TODO: Introduce a specific error for this case.
|
|
|
|
return StatusValue::newFatal( 'cite_error_ref_too_many_keys' );
|
|
|
|
}
|
|
|
|
|
2019-12-16 14:33:55 +00:00
|
|
|
if ( $dir !== null && !in_array( strtolower( $dir ), [ 'ltr', 'rtl' ] ) ) {
|
|
|
|
return StatusValue::newFatal( 'cite_error_ref_invalid_dir', $dir );
|
|
|
|
}
|
|
|
|
|
2019-12-06 09:35:55 +00:00
|
|
|
return $this->inReferencesGroup === null ?
|
|
|
|
$this->validateRefOutsideOfReferences( $text, $name ) :
|
2019-12-19 13:20:10 +00:00
|
|
|
$this->validateRefInReferences( $text, $group, $name );
|
2019-12-06 09:35:55 +00:00
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
|
2020-01-20 10:13:31 +00:00
|
|
|
/**
|
|
|
|
* @param ?string $text
|
|
|
|
* @param ?string $name
|
|
|
|
*
|
|
|
|
* @return StatusValue
|
|
|
|
*/
|
2019-12-06 09:35:55 +00:00
|
|
|
private function validateRefOutsideOfReferences(
|
|
|
|
?string $text,
|
|
|
|
?string $name
|
2019-12-19 13:20:10 +00:00
|
|
|
) : StatusValue {
|
2019-12-06 09:35:55 +00:00
|
|
|
if ( !$name ) {
|
2020-01-20 10:40:12 +00:00
|
|
|
if ( $text === null ) {
|
2020-01-17 09:40:42 +00:00
|
|
|
// Completely empty ref like <ref /> is forbidden.
|
2019-12-06 09:35:55 +00:00
|
|
|
return StatusValue::newFatal( 'cite_error_ref_no_key' );
|
|
|
|
} elseif ( trim( $text ) === '' ) {
|
|
|
|
// Must have content or reuse another ref by name.
|
|
|
|
return StatusValue::newFatal( 'cite_error_ref_no_input' );
|
2019-11-22 21:45:11 +00:00
|
|
|
}
|
2019-12-06 09:35:55 +00:00
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
|
2019-12-21 01:25:58 +00:00
|
|
|
if ( $text !== null && preg_match(
|
2019-12-09 10:29:40 +00:00
|
|
|
'/<ref(erences)?\b[^>]*+>/i',
|
|
|
|
preg_replace( '#<(\w++)[^>]*+>.*?</\1\s*>|<!--.*?-->#s', '', $text )
|
2019-12-06 09:35:55 +00:00
|
|
|
) ) {
|
|
|
|
// (bug T8199) This most likely implies that someone left off the
|
|
|
|
// closing </ref> tag, which will cause the entire article to be
|
|
|
|
// eaten up until the next <ref>. So we bail out early instead.
|
|
|
|
// The fancy regex above first tries chopping out anything that
|
|
|
|
// looks like a comment or SGML tag, which is a crude way to avoid
|
|
|
|
// false alarms for <nowiki>, <pre>, etc.
|
|
|
|
//
|
|
|
|
// Possible improvement: print the warning, followed by the contents
|
|
|
|
// of the <ref> tag. This way no part of the article will be eaten
|
|
|
|
// even temporarily.
|
|
|
|
return StatusValue::newFatal( 'cite_error_included_ref' );
|
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
|
2019-12-06 09:35:55 +00:00
|
|
|
// TODO: Assert things such as $text is different than existing ref with $name,
|
|
|
|
// currently done in `pushRef`.
|
|
|
|
|
|
|
|
return StatusValue::newGood();
|
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
|
2020-01-20 10:13:31 +00:00
|
|
|
/**
|
|
|
|
* @param ?string $text
|
|
|
|
* @param string $group
|
|
|
|
* @param ?string $name
|
|
|
|
*
|
|
|
|
* @return StatusValue
|
|
|
|
*/
|
2019-12-06 09:35:55 +00:00
|
|
|
private function validateRefInReferences(
|
|
|
|
?string $text,
|
2019-12-19 13:20:10 +00:00
|
|
|
string $group,
|
|
|
|
?string $name
|
|
|
|
) : StatusValue {
|
2019-12-06 09:35:55 +00:00
|
|
|
// FIXME: Some assertions make assumptions that rely on earlier tests not failing.
|
|
|
|
// These dependencies need to be explicit so they aren't accidentally broken by
|
|
|
|
// reordering in the future, or made more robust to initial conditions.
|
|
|
|
|
|
|
|
if ( $group !== $this->inReferencesGroup ) {
|
|
|
|
// <ref> and <references> have conflicting group attributes.
|
|
|
|
return StatusValue::newFatal( 'cite_error_references_group_mismatch',
|
|
|
|
Sanitizer::safeEncodeAttribute( $group ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !$name ) {
|
|
|
|
// <ref> calls inside <references> must be named
|
|
|
|
return StatusValue::newFatal( 'cite_error_references_no_key' );
|
|
|
|
}
|
|
|
|
|
2019-12-17 12:04:50 +00:00
|
|
|
if ( $text === null || trim( $text ) === '' ) {
|
2019-12-06 09:35:55 +00:00
|
|
|
// <ref> called in <references> has no content.
|
|
|
|
return StatusValue::newFatal(
|
|
|
|
'cite_error_empty_references_define',
|
|
|
|
Sanitizer::safeEncodeAttribute( $name )
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Section previews are exempt from some rules.
|
|
|
|
if ( !$this->isSectionPreview ) {
|
|
|
|
if ( !$this->referenceStack->hasGroup( $group ) ) {
|
|
|
|
// Called with group attribute not defined in text.
|
|
|
|
return StatusValue::newFatal( 'cite_error_references_missing_group',
|
|
|
|
Sanitizer::safeEncodeAttribute( $group ) );
|
2019-11-22 21:45:11 +00:00
|
|
|
}
|
2019-11-26 14:08:18 +00:00
|
|
|
|
2019-12-06 09:35:55 +00:00
|
|
|
$groupRefs = $this->referenceStack->getGroupRefs( $group );
|
|
|
|
|
|
|
|
if ( !isset( $groupRefs[$name] ) ) {
|
|
|
|
// No such named ref exists in this group.
|
|
|
|
return StatusValue::newFatal( 'cite_error_references_missing_key',
|
|
|
|
Sanitizer::safeEncodeAttribute( $name ) );
|
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return StatusValue::newGood();
|
|
|
|
}
|
|
|
|
|
2011-05-28 20:44:24 +00:00
|
|
|
/**
|
2019-11-25 15:10:05 +00:00
|
|
|
* TODO: Looks like this should be split into a section insensitive to context, and the
|
|
|
|
* special handling for each context.
|
|
|
|
*
|
2019-12-19 09:16:14 +00:00
|
|
|
* @param Parser $parser
|
2019-12-19 13:20:10 +00:00
|
|
|
* @param ?string $text Raw, untrimmed wikitext content of the <ref> tag, if any
|
|
|
|
* @param string[] $argv Arguments as given in <ref name=…>, already trimmed
|
2015-10-26 14:44:02 +00:00
|
|
|
*
|
2019-12-10 15:16:15 +00:00
|
|
|
* @return string HTML
|
2011-05-28 20:44:24 +00:00
|
|
|
*/
|
2016-09-21 11:01:25 +00:00
|
|
|
private function guardedRef(
|
2019-12-19 09:16:14 +00:00
|
|
|
Parser $parser,
|
2019-11-28 10:15:19 +00:00
|
|
|
?string $text,
|
2019-12-19 09:16:14 +00:00
|
|
|
array $argv
|
2019-11-28 10:15:19 +00:00
|
|
|
) : string {
|
2019-11-26 10:46:05 +00:00
|
|
|
// Tag every page where Book Referencing has been used, whether or not the ref tag is valid.
|
|
|
|
// This code and the page property will be removed once the feature is stable. See T237531.
|
|
|
|
if ( array_key_exists( self::BOOK_REF_ATTRIBUTE, $argv ) ) {
|
|
|
|
$parser->getOutput()->setProperty( self::BOOK_REF_PROPERTY, true );
|
|
|
|
}
|
|
|
|
|
2019-11-26 11:05:51 +00:00
|
|
|
$status = $this->parseArguments(
|
2019-11-27 16:47:20 +00:00
|
|
|
$argv,
|
2019-12-19 09:16:14 +00:00
|
|
|
[ 'group', 'name', self::BOOK_REF_ATTRIBUTE, 'follow', 'dir' ]
|
2019-11-27 16:47:20 +00:00
|
|
|
);
|
2019-12-19 09:16:14 +00:00
|
|
|
$arguments = $status->getValue();
|
2019-11-26 11:05:51 +00:00
|
|
|
// Use the default group, or the references group when inside one.
|
2019-12-19 09:16:14 +00:00
|
|
|
if ( $arguments['group'] === null ) {
|
|
|
|
$arguments['group'] = $this->inReferencesGroup ?? self::DEFAULT_GROUP;
|
2009-07-26 22:15:13 +00:00
|
|
|
}
|
2011-02-22 00:07:21 +00:00
|
|
|
|
2019-12-19 09:16:14 +00:00
|
|
|
$status->merge( $this->validateRef( $text, ...array_values( $arguments ) ) );
|
2009-07-26 22:15:13 +00:00
|
|
|
|
2020-01-20 11:30:21 +00:00
|
|
|
if ( !$status->isGood() && $this->inReferencesGroup !== null ) {
|
|
|
|
foreach ( $status->getErrors() as $error ) {
|
|
|
|
$this->mReferencesErrors[] = $this->errorReporter->halfParsed(
|
|
|
|
$parser,
|
|
|
|
$error['message'],
|
|
|
|
...$error['params']
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2019-11-26 09:31:41 +00:00
|
|
|
// Validation cares about the difference between null and empty, but from here on we don't
|
|
|
|
if ( $text !== null && trim( $text ) === '' ) {
|
|
|
|
$text = null;
|
|
|
|
}
|
2019-12-19 13:20:10 +00:00
|
|
|
[ 'group' => $group, 'name' => $name ] = $arguments;
|
|
|
|
|
2019-11-22 21:45:11 +00:00
|
|
|
if ( $this->inReferencesGroup !== null ) {
|
2020-01-13 13:29:23 +00:00
|
|
|
$groupRefs = $this->referenceStack->getGroupRefs( $group );
|
2020-01-20 11:30:21 +00:00
|
|
|
// In preview mode, it's possible to reach this with the ref *not* being known
|
|
|
|
if ( $text === null || !isset( $groupRefs[$name] ) ) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !isset( $groupRefs[$name]['text'] ) ) {
|
|
|
|
$this->referenceStack->appendText( $group, $name, $text );
|
|
|
|
} elseif ( $groupRefs[$name]['text'] !== $text ) {
|
|
|
|
// two refs with same key and different content
|
|
|
|
// adds error message to the original ref
|
|
|
|
// TODO: report these errors the same way as the others, rather than a
|
|
|
|
// special case to append to the second one's content.
|
|
|
|
$this->referenceStack->appendText(
|
|
|
|
$group,
|
|
|
|
$name,
|
|
|
|
' ' . $this->errorReporter->plain(
|
2019-12-09 14:07:37 +00:00
|
|
|
$parser,
|
2020-01-20 11:30:21 +00:00
|
|
|
'cite_error_references_duplicate_key',
|
|
|
|
$name
|
|
|
|
)
|
|
|
|
);
|
2009-07-26 22:15:13 +00:00
|
|
|
}
|
2019-11-22 21:45:11 +00:00
|
|
|
return '';
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
2011-02-22 00:07:21 +00:00
|
|
|
|
2020-01-20 11:30:57 +00:00
|
|
|
if ( !$status->isGood() ) {
|
2019-11-22 17:28:51 +00:00
|
|
|
$this->referenceStack->pushInvalidRef();
|
2008-09-18 17:16:10 +00:00
|
|
|
|
2019-11-26 09:13:18 +00:00
|
|
|
// FIXME: If we ever have multiple errors, these must all be presented to the user,
|
|
|
|
// so they know what to correct.
|
2019-11-25 15:10:05 +00:00
|
|
|
// TODO: Make this nicer, see T238061
|
2019-11-27 16:47:20 +00:00
|
|
|
$error = $status->getErrors()[0];
|
2019-12-11 15:05:19 +00:00
|
|
|
return $this->errorReporter->halfParsed( $parser, $error['message'], ...$error['params'] );
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
|
|
|
|
2019-11-29 23:26:58 +00:00
|
|
|
$ref = $this->referenceStack->pushRef(
|
2019-12-19 09:16:14 +00:00
|
|
|
$parser, $parser->getStripState(), $text, $argv, ...array_values( $arguments ) );
|
2019-12-12 15:42:53 +00:00
|
|
|
return $ref
|
|
|
|
? $this->footnoteMarkFormatter->linkRef( $parser, $group, $ref )
|
|
|
|
: '';
|
2019-11-01 14:29:33 +00:00
|
|
|
}
|
|
|
|
|
2008-06-06 20:38:04 +00:00
|
|
|
/**
|
2015-10-26 14:44:02 +00:00
|
|
|
* @param string[] $argv The argument vector
|
2019-11-27 12:50:05 +00:00
|
|
|
* @param string[] $allowedAttributes Allowed attribute names
|
|
|
|
*
|
2019-11-26 11:05:51 +00:00
|
|
|
* @return StatusValue Either an error, or has a value with the dictionary of field names and
|
|
|
|
* parsed or default values. Missing attributes will be `null`.
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
2019-11-28 10:15:19 +00:00
|
|
|
private function parseArguments( array $argv, array $allowedAttributes ) : StatusValue {
|
2019-11-26 11:05:51 +00:00
|
|
|
$maxCount = count( $allowedAttributes );
|
|
|
|
$allValues = array_merge( array_fill_keys( $allowedAttributes, null ), $argv );
|
|
|
|
$status = StatusValue::newGood( array_slice( $allValues, 0, $maxCount ) );
|
|
|
|
|
|
|
|
if ( count( $allValues ) > $maxCount ) {
|
|
|
|
// A <ref> must have a name (can be null), but <references> can't have one
|
|
|
|
$status->fatal( in_array( 'name', $allowedAttributes )
|
|
|
|
? 'cite_error_ref_too_many_keys'
|
|
|
|
: 'cite_error_references_invalid_parameters'
|
|
|
|
);
|
2018-11-19 15:29:16 +00:00
|
|
|
}
|
|
|
|
|
2019-11-26 11:05:51 +00:00
|
|
|
return $status;
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
|
|
|
|
2015-06-08 14:05:06 +00:00
|
|
|
/**
|
|
|
|
* Callback function for <references>
|
2008-06-06 20:38:04 +00:00
|
|
|
*
|
2015-10-26 14:44:02 +00:00
|
|
|
* @param Parser $parser
|
2019-12-19 13:20:10 +00:00
|
|
|
* @param ?string $text Raw, untrimmed wikitext content of the <references> tag, if any
|
|
|
|
* @param string[] $argv Arguments as given in <references name=…>, already trimmed
|
2011-05-28 20:44:24 +00:00
|
|
|
*
|
2019-11-12 12:06:39 +00:00
|
|
|
* @return string|false False in case a <references> tag is not allowed in the current context
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
2019-12-19 13:20:10 +00:00
|
|
|
public function references( Parser $parser, ?string $text, array $argv ) {
|
2019-11-19 13:34:19 +00:00
|
|
|
if ( $this->mInCite || $this->inReferencesGroup !== null ) {
|
2019-11-12 12:06:39 +00:00
|
|
|
return false;
|
2015-07-18 20:55:32 +00:00
|
|
|
}
|
2019-11-11 19:16:05 +00:00
|
|
|
|
2019-12-19 13:20:10 +00:00
|
|
|
$ret = $this->guardedReferences( $parser, $text, $argv );
|
2019-11-19 13:34:19 +00:00
|
|
|
$this->inReferencesGroup = null;
|
2019-11-11 19:16:05 +00:00
|
|
|
|
2015-07-18 20:55:32 +00:00
|
|
|
return $ret;
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
|
|
|
|
2011-05-28 20:44:24 +00:00
|
|
|
/**
|
2018-07-24 23:08:25 +00:00
|
|
|
* Must only be called from references(). Use that to prevent recursion.
|
|
|
|
*
|
2015-10-26 14:44:02 +00:00
|
|
|
* @param Parser $parser
|
2019-12-19 13:20:10 +00:00
|
|
|
* @param ?string $text Raw, untrimmed wikitext content of the <references> tag, if any
|
|
|
|
* @param string[] $argv Arguments as given in <references name=…>, already trimmed
|
2018-11-19 15:24:21 +00:00
|
|
|
*
|
2019-12-10 15:16:15 +00:00
|
|
|
* @return string HTML
|
2011-05-28 20:44:24 +00:00
|
|
|
*/
|
2016-09-21 11:01:25 +00:00
|
|
|
private function guardedReferences(
|
2019-12-19 13:20:10 +00:00
|
|
|
Parser $parser,
|
2019-11-28 10:15:19 +00:00
|
|
|
?string $text,
|
2019-12-19 13:20:10 +00:00
|
|
|
array $argv
|
2019-11-28 10:15:19 +00:00
|
|
|
) : string {
|
2019-12-09 14:07:37 +00:00
|
|
|
$status = $this->parseArguments( $argv, [ 'group', 'responsive' ] );
|
2019-11-26 11:05:51 +00:00
|
|
|
[ 'group' => $group, 'responsive' => $responsive ] = $status->getValue();
|
2019-11-27 12:50:05 +00:00
|
|
|
$this->inReferencesGroup = $group ?? self::DEFAULT_GROUP;
|
2011-02-22 00:07:21 +00:00
|
|
|
|
2019-11-26 09:31:41 +00:00
|
|
|
if ( $text !== null && trim( $text ) !== '' ) {
|
2020-01-14 09:24:28 +00:00
|
|
|
if ( substr_count( $text, Parser::MARKER_PREFIX . "-references-" ) ) {
|
|
|
|
return $this->errorReporter->halfParsed( $parser, 'cite_error_included_references' );
|
|
|
|
}
|
|
|
|
|
2020-01-09 10:53:43 +00:00
|
|
|
// Detect whether we were sent already rendered <ref>s. Mostly a side effect of using
|
|
|
|
// {{#tag:references}}. The following assumes that the parsed <ref>s sent within the
|
|
|
|
// <references> block were the most recent calls to <ref>. This assumption is true for
|
|
|
|
// all known use cases, but not strictly enforced by the parser. It is possible that
|
|
|
|
// some unusual combination of #tag, <references> and conditional parser functions could
|
|
|
|
// be created that would lead to malformed references here.
|
2019-11-05 09:28:20 +00:00
|
|
|
$count = substr_count( $text, Parser::MARKER_PREFIX . "-ref-" );
|
Rollback all, then redo all, when fixing out-of-order tags; not one-by-one
Imagine the following wikitext:
<ref name=r/>
<references>
<ref name=r>A</ref>
<ref name=r>B</ref>
</references>
This is simple. Cite would see these as the following operations, in
order:
1. Use only: <ref name=r/>
2. References block
3. Define only: <ref name=r>A</ref>
4. Define only: <ref name=r>B</ref>
<ref name=r> is defined twice with different content and we get an
error message.
Now, imagine the following wikitext:
<ref name=r/>
{{#tag:references|
<ref name=r>A</ref>
<ref name=r>B</ref>
}}
Cite would see these as the following operations, in order:
1. Use only: <ref name=r/>
2. Use and define: <ref name=r>A</ref>
3. Use and define: <ref name=r>B</ref>
4. References block
When the 'references' block appears, Cite notices that the tag has
parsed content, and deduces that it was called with #tag. We need to
undo the last operations to update internal bookkeeping, as the last
two 'ref' tags do not actually represent ref usages, as we assumed,
but only definitions.
5. Undo: <ref name=r> reused
6. Define only: <ref name=r>B</ref>
7. Undo: <ref name=r> defined
(Right now, it appears to Cite that <ref name=r> was never defined!)
8. Define only: <ref name=r>A</ref>
Thus we get no errors, although we should.
This patch changes the order of the rollback operations:
5. Undo: <ref name=r> reused
6. Undo: <ref name=r> defined
7. Define only: <ref name=r>A</ref>
8. Define only: <ref name=r>B</ref>
Aha! <ref name=r> is defined twice with different content! We get an
error correctly.
Bug: T124227
Change-Id: I61766c4104856323987cca9a5e4ff85a76b3618b
2016-01-20 20:38:59 +00:00
|
|
|
|
2020-01-09 10:53:43 +00:00
|
|
|
// Undo effects of calling <ref> while unaware of being contained in <references>
|
2019-12-19 13:20:10 +00:00
|
|
|
foreach ( $this->referenceStack->rollbackRefs( $count ) as $call ) {
|
2020-01-09 10:53:43 +00:00
|
|
|
// Rerun <ref> call with the <references> context now being known
|
2019-12-19 09:16:14 +00:00
|
|
|
$this->guardedRef( $parser, ...$call );
|
2015-06-08 14:05:06 +00:00
|
|
|
}
|
|
|
|
|
2020-01-09 10:53:43 +00:00
|
|
|
// Parse the <references> content to process any unparsed <ref> tags
|
2019-11-05 09:28:20 +00:00
|
|
|
$parser->recursiveTagParse( $text );
|
2009-07-26 22:15:13 +00:00
|
|
|
}
|
|
|
|
|
2020-01-20 11:30:57 +00:00
|
|
|
if ( !$status->isGood() ) {
|
2019-11-26 11:05:51 +00:00
|
|
|
// Bail out with an error.
|
|
|
|
$error = $status->getErrors()[0];
|
2019-12-11 15:05:19 +00:00
|
|
|
return $this->errorReporter->halfParsed( $parser, $error['message'], ...$error['params'] );
|
2015-07-18 20:55:32 +00:00
|
|
|
}
|
2011-02-22 00:07:21 +00:00
|
|
|
|
2019-12-11 15:02:49 +00:00
|
|
|
$s = $this->formatReferences( $parser, $this->inReferencesGroup, $responsive );
|
2015-07-18 20:55:32 +00:00
|
|
|
|
2020-01-09 10:53:43 +00:00
|
|
|
// Append errors generated while processing <references>
|
2015-07-07 07:37:14 +00:00
|
|
|
if ( $this->mReferencesErrors ) {
|
2015-07-18 20:55:32 +00:00
|
|
|
$s .= "\n" . implode( "<br />\n", $this->mReferencesErrors );
|
2016-05-09 23:36:49 +00:00
|
|
|
$this->mReferencesErrors = [];
|
2015-07-18 20:55:32 +00:00
|
|
|
}
|
|
|
|
return $s;
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-12-11 15:02:49 +00:00
|
|
|
* @param Parser $parser
|
2015-10-26 14:44:02 +00:00
|
|
|
* @param string $group
|
2019-12-09 14:07:37 +00:00
|
|
|
* @param string|null $responsive Defaults to $wgCiteResponsiveReferences when not set
|
2019-12-10 15:16:15 +00:00
|
|
|
*
|
|
|
|
* @return string HTML
|
2008-06-06 20:38:04 +00:00
|
|
|
*/
|
2019-12-09 14:07:37 +00:00
|
|
|
private function formatReferences(
|
|
|
|
Parser $parser,
|
|
|
|
string $group,
|
|
|
|
string $responsive = null
|
|
|
|
) : string {
|
|
|
|
global $wgCiteResponsiveReferences;
|
2011-02-22 00:07:21 +00:00
|
|
|
|
2019-12-09 14:07:37 +00:00
|
|
|
return $this->referencesFormatter->formatReferences(
|
|
|
|
$parser,
|
|
|
|
$this->referenceStack->popGroup( $group ),
|
2019-12-27 18:25:25 +00:00
|
|
|
$responsive !== null ? $responsive !== '0' : $wgCiteResponsiveReferences,
|
2019-12-09 14:07:37 +00:00
|
|
|
$this->isSectionPreview
|
|
|
|
);
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|
|
|
|
|
2009-01-26 18:02:28 +00:00
|
|
|
/**
|
2015-08-06 17:33:01 +00:00
|
|
|
* Called at the end of page processing to append a default references
|
|
|
|
* section, if refs were used without a main references tag. If there are references
|
|
|
|
* in a custom group, and there is no references tag for it, show an error
|
|
|
|
* message for that group.
|
2014-05-18 23:06:44 +00:00
|
|
|
* If we are processing a section preview, this adds the missing
|
|
|
|
* references tags and does not add the errors.
|
2011-05-28 20:44:24 +00:00
|
|
|
*
|
2019-12-11 15:02:49 +00:00
|
|
|
* @param Parser $parser
|
2019-11-28 10:15:19 +00:00
|
|
|
* @param bool $isSectionPreview
|
2019-12-10 15:16:15 +00:00
|
|
|
*
|
|
|
|
* @return string HTML
|
2009-01-26 18:02:28 +00:00
|
|
|
*/
|
2019-12-11 15:02:49 +00:00
|
|
|
public function checkRefsNoReferences( Parser $parser, bool $isSectionPreview ) : string {
|
2016-02-06 12:24:58 +00:00
|
|
|
$s = '';
|
2019-12-10 15:16:15 +00:00
|
|
|
foreach ( $this->referenceStack->getGroups() as $group ) {
|
|
|
|
if ( $group === self::DEFAULT_GROUP || $isSectionPreview ) {
|
2019-12-09 14:07:37 +00:00
|
|
|
$s .= $this->formatReferences( $parser, $group );
|
2019-12-10 15:16:15 +00:00
|
|
|
} else {
|
|
|
|
$s .= "\n<br />" . $this->errorReporter->halfParsed(
|
2019-12-11 15:05:19 +00:00
|
|
|
$parser,
|
2019-12-10 15:16:15 +00:00
|
|
|
'cite_error_group_refs_without_references',
|
|
|
|
Sanitizer::safeEncodeAttribute( $group )
|
|
|
|
);
|
2009-01-26 18:02:28 +00:00
|
|
|
}
|
|
|
|
}
|
2019-11-28 10:15:19 +00:00
|
|
|
if ( $isSectionPreview && $s !== '' ) {
|
2019-12-06 12:09:28 +00:00
|
|
|
$headerMsg = wfMessage( 'cite_section_preview_references' );
|
2016-02-08 16:02:47 +00:00
|
|
|
if ( !$headerMsg->isDisabled() ) {
|
2019-12-09 14:07:37 +00:00
|
|
|
$s = Html::element(
|
|
|
|
'h2',
|
|
|
|
[ 'id' => 'mw-ext-cite-cite_section_preview_references_header' ],
|
|
|
|
$headerMsg->text()
|
|
|
|
) . $s;
|
2016-02-08 16:02:47 +00:00
|
|
|
}
|
2019-12-03 14:35:24 +00:00
|
|
|
// provide a preview of references in its own section
|
2019-12-09 14:07:37 +00:00
|
|
|
$s = "\n" . Html::rawElement(
|
|
|
|
'div',
|
|
|
|
[ 'class' => 'mw-ext-cite-cite_section_preview_references' ],
|
|
|
|
$s
|
|
|
|
);
|
2016-02-06 12:24:58 +00:00
|
|
|
}
|
2019-12-03 14:35:24 +00:00
|
|
|
return $s;
|
2009-01-26 18:02:28 +00:00
|
|
|
}
|
2011-09-14 15:07:20 +00:00
|
|
|
|
2019-12-20 18:31:54 +00:00
|
|
|
/**
|
|
|
|
* @see https://phabricator.wikimedia.org/T240248
|
|
|
|
*/
|
2019-12-10 13:13:16 +00:00
|
|
|
public function __clone() {
|
2019-12-20 18:31:54 +00:00
|
|
|
throw new LogicException( 'Create a new instance please' );
|
2019-12-10 13:13:16 +00:00
|
|
|
}
|
|
|
|
|
2008-06-06 20:38:04 +00:00
|
|
|
}
|