"PhanUndeclaredConstant",
// approximate error count: 60
"PhanTypeMismatchArgument",
- // approximate error count: 219
- "PhanUndeclaredMethod",
// approximate error count: 752
"PhanUndeclaredProperty",
] );
--- /dev/null
+<?php
+// These stubs were generated by the phan stub generator.
+// @phan-stub-for-extension dom@20031129
+
+namespace {
+class DOMAttr extends \DOMNode {
+
+ // properties
+ public $name;
+ public $ownerElement;
+ public $schemaTypeInfo;
+ public $specified;
+ public $value;
+
+ // methods
+ public function isId() {}
+ public function __construct($name, $value = null) {}
+}
+
+class DOMCdataSection extends \DOMText {
+
+ // methods
+ public function __construct($value) {}
+}
+
+class DOMCharacterData extends \DOMNode {
+
+ // properties
+ public $data;
+ public $length;
+
+ // methods
+ public function substringData($offset, $count) {}
+ public function appendData($arg) {}
+ public function insertData($offset, $arg) {}
+ public function deleteData($offset, $count) {}
+ public function replaceData($offset, $count, $arg) {}
+}
+
+class DOMComment extends \DOMCharacterData {
+
+ // methods
+ public function __construct($value = null) {}
+}
+
+class DOMConfiguration {
+
+ // methods
+ public function setParameter($name, $value) {}
+ public function getParameter($name = null) {}
+ public function canSetParameter($name = null, $value = null) {}
+}
+
+class DOMDocument extends \DOMNode {
+
+ // properties
+ public $actualEncoding;
+ public $config;
+ public $doctype;
+ public $documentElement;
+ public $documentURI;
+ public $encoding;
+ public $formatOutput;
+ public $implementation;
+ public $preserveWhiteSpace;
+ public $recover;
+ public $resolveExternals;
+ public $standalone;
+ public $strictErrorChecking;
+ public $substituteEntities;
+ public $validateOnParse;
+ public $version;
+ public $xmlEncoding;
+ public $xmlStandalone;
+ public $xmlVersion;
+
+ // methods
+ public function createElement($tagName, $value = null) {}
+ public function createDocumentFragment() {}
+ public function createTextNode($data) {}
+ public function createComment($data) {}
+ public function createCDATASection($data) {}
+ public function createProcessingInstruction($target, $data) {}
+ public function createAttribute($name) {}
+ public function createEntityReference($name) {}
+ public function getElementsByTagName($tagName) {}
+ public function importNode(\DOMNode $importedNode, $deep) {}
+ public function createElementNS($namespaceURI, $qualifiedName, $value = null) {}
+ public function createAttributeNS($namespaceURI, $qualifiedName) {}
+ public function getElementsByTagNameNS($namespaceURI, $localName) {}
+ public function getElementById($elementId) {}
+ public function adoptNode(\DOMNode $source) {}
+ public function normalizeDocument() {}
+ public function renameNode(\DOMNode $node, $namespaceURI, $qualifiedName) {}
+ public function load($source, $options = null) {}
+ public function save($file) {}
+ public function loadXML($source, $options = null) {}
+ public function saveXML(\DOMNode $node = null, $options = null) {}
+ public function __construct($version = null, $encoding = null) {}
+ public function validate() {}
+ public function xinclude($options = null) {}
+ public function loadHTML($source, $options = null) {}
+ public function loadHTMLFile($source, $options = null) {}
+ public function saveHTML() {}
+ public function saveHTMLFile($file) {}
+ public function schemaValidate($filename) {}
+ public function schemaValidateSource($source) {}
+ public function relaxNGValidate($filename) {}
+ public function relaxNGValidateSource($source) {}
+ public function registerNodeClass($baseClass, $extendedClass) {}
+}
+
+class DOMDocumentFragment extends \DOMNode {
+
+ // properties
+ public $name;
+
+ // methods
+ public function __construct() {}
+ public function appendXML($data) {}
+}
+
+class DOMDocumentType extends \DOMNode {
+
+ // properties
+ public $entities;
+ public $internalSubset;
+ public $name;
+ public $notations;
+ public $publicId;
+ public $systemId;
+}
+
+class DOMDomError {
+}
+
+class DOMElement extends \DOMNode {
+
+ // properties
+ public $schemaTypeInfo;
+ public $tagName;
+
+ // methods
+ public function getAttribute($name) {}
+ public function setAttribute($name, $value) {}
+ public function removeAttribute($name) {}
+ public function getAttributeNode($name) {}
+ public function setAttributeNode(\DOMAttr $newAttr) {}
+ public function removeAttributeNode(\DOMAttr $oldAttr) {}
+ public function getElementsByTagName($name) {}
+ public function getAttributeNS($namespaceURI, $localName) {}
+ public function setAttributeNS($namespaceURI, $qualifiedName, $value) {}
+ public function removeAttributeNS($namespaceURI, $localName) {}
+ public function getAttributeNodeNS($namespaceURI, $localName) {}
+ public function setAttributeNodeNS(\DOMAttr $newAttr) {}
+ public function getElementsByTagNameNS($namespaceURI, $localName) {}
+ public function hasAttribute($name) {}
+ public function hasAttributeNS($namespaceURI, $localName) {}
+ public function setIdAttribute($name, $isId) {}
+ public function setIdAttributeNS($namespaceURI, $localName, $isId) {}
+ public function setIdAttributeNode(\DOMAttr $attr, $isId) {}
+ public function __construct($name, $value = null, $uri = null) {}
+}
+
+class DOMEntity extends \DOMNode {
+
+ // properties
+ public $actualEncoding;
+ public $encoding;
+ public $notationName;
+ public $publicId;
+ public $systemId;
+ public $version;
+}
+
+class DOMEntityReference extends \DOMNode {
+
+ // properties
+ public $name;
+
+ // methods
+ public function __construct($name) {}
+}
+
+class DOMErrorHandler {
+
+ // methods
+ public function handleError(\DOMDomError $error) {}
+}
+
+final class DOMException extends \Exception {
+
+ // properties
+ public $code;
+ protected $message;
+ protected $file;
+ protected $line;
+}
+
+class DOMImplementation {
+
+ // properties
+ public $name;
+
+ // methods
+ public function getFeature($feature, $version) {}
+ public function hasFeature() {}
+ public function createDocumentType($qualifiedName, $publicId, $systemId) {}
+ public function createDocument($namespaceURI, $qualifiedName, \DOMDocumentType $docType) {}
+}
+
+class DOMImplementationList {
+
+ // methods
+ public function item($index) {}
+}
+
+class DOMImplementationSource {
+
+ // methods
+ public function getDomimplementation($features) {}
+ public function getDomimplementations($features) {}
+}
+
+class DOMLocator {
+}
+
+class DOMNameList {
+
+ // methods
+ public function getName($index) {}
+ public function getNamespaceURI($index) {}
+}
+
+class DOMNameSpaceNode {
+}
+
+class DOMNamedNodeMap implements \Traversable, \Countable {
+
+ // properties
+ public $length;
+
+ // methods
+ public function getNamedItem($name) {}
+ public function setNamedItem(\DOMNode $arg) {}
+ public function removeNamedItem($name = null) {}
+ public function item($index = null) {}
+ public function getNamedItemNS($namespaceURI = null, $localName = null) {}
+ public function setNamedItemNS(\DOMNode $arg = null) {}
+ public function removeNamedItemNS($namespaceURI = null, $localName = null) {}
+ public function count() {}
+}
+
+class DOMNode {
+
+ // properties
+ public $attributes;
+ public $baseURI;
+ public $childNodes;
+ public $firstChild;
+ public $lastChild;
+ public $localName;
+ public $namespaceURI;
+ public $nextSibling;
+ public $nodeName;
+ public $nodeType;
+ public $nodeValue;
+ public $ownerDocument;
+ public $parentNode;
+ public $prefix;
+ public $previousSibling;
+ public $textContent;
+
+ // methods
+ public function insertBefore(\DOMNode $newChild, \DOMNode $refChild = null) {}
+ public function replaceChild(\DOMNode $newChild, \DOMNode $oldChild) {}
+ public function removeChild(\DOMNode $oldChild) {}
+ public function appendChild(\DOMNode $newChild) {}
+ public function hasChildNodes() {}
+ public function cloneNode($deep = null) {}
+ public function normalize() {}
+ public function isSupported($feature, $version) {}
+ public function hasAttributes() {}
+ public function compareDocumentPosition(\DOMNode $other) {}
+ public function isSameNode(\DOMNode $other) {}
+ public function lookupPrefix($namespaceURI) {}
+ public function isDefaultNamespace($namespaceURI) {}
+ public function lookupNamespaceUri($prefix) {}
+ public function isEqualNode(\DOMNode $arg) {}
+ public function getFeature($feature, $version) {}
+ public function setUserData($key, $data, $handler) {}
+ public function getUserData($key) {}
+ public function getNodePath() {}
+ public function getLineNo() {}
+ public function C14N($exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null) {}
+ public function C14NFile($uri, $exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null) {}
+}
+
+class DOMNodeList implements \Traversable, \Countable {
+
+ // properties
+ public $length;
+
+ // methods
+ public function item($index) {}
+ public function count() {}
+}
+
+class DOMNotation extends \DOMNode {
+
+ // properties
+ public $publicId;
+ public $systemId;
+}
+
+class DOMProcessingInstruction extends \DOMNode {
+
+ // properties
+ public $data;
+ public $target;
+
+ // methods
+ public function __construct($name, $value = null) {}
+}
+
+class DOMStringExtend {
+
+ // methods
+ public function findOffset16($offset32) {}
+ public function findOffset32($offset16) {}
+}
+
+class DOMStringList {
+
+ // methods
+ public function item($index) {}
+}
+
+class DOMText extends \DOMCharacterData {
+
+ // properties
+ public $wholeText;
+
+ // methods
+ public function splitText($offset) {}
+ public function isWhitespaceInElementContent() {}
+ public function isElementContentWhitespace() {}
+ public function replaceWholeText($content) {}
+ public function __construct($value = null) {}
+}
+
+class DOMTypeinfo {
+}
+
+class DOMUserDataHandler {
+
+ // methods
+ public function handle() {}
+}
+
+class DOMXPath {
+
+ // properties
+ public $document;
+
+ // methods
+ public function __construct(\DOMDocument $doc) {}
+ public function registerNamespace($prefix, $uri) {}
+ public function query($expr, \DOMNode $context = null, $registerNodeNS = null) {}
+ public function evaluate($expr, \DOMNode $context = null, $registerNodeNS = null) {}
+ public function registerPhpFunctions() {}
+}
+
+function dom_import_simplexml($node) {}
+const DOMSTRING_SIZE_ERR = 2;
+const DOM_HIERARCHY_REQUEST_ERR = 3;
+const DOM_INDEX_SIZE_ERR = 1;
+const DOM_INUSE_ATTRIBUTE_ERR = 10;
+const DOM_INVALID_ACCESS_ERR = 15;
+const DOM_INVALID_CHARACTER_ERR = 5;
+const DOM_INVALID_MODIFICATION_ERR = 13;
+const DOM_INVALID_STATE_ERR = 11;
+const DOM_NAMESPACE_ERR = 14;
+const DOM_NOT_FOUND_ERR = 8;
+const DOM_NOT_SUPPORTED_ERR = 9;
+const DOM_NO_DATA_ALLOWED_ERR = 6;
+const DOM_NO_MODIFICATION_ALLOWED_ERR = 7;
+const DOM_PHP_ERR = 0;
+const DOM_SYNTAX_ERR = 12;
+const DOM_VALIDATION_ERR = 16;
+const DOM_WRONG_DOCUMENT_ERR = 4;
+const XML_ATTRIBUTE_CDATA = 1;
+const XML_ATTRIBUTE_DECL_NODE = 16;
+const XML_ATTRIBUTE_ENTITY = 6;
+const XML_ATTRIBUTE_ENUMERATION = 9;
+const XML_ATTRIBUTE_ID = 2;
+const XML_ATTRIBUTE_IDREF = 3;
+const XML_ATTRIBUTE_IDREFS = 4;
+const XML_ATTRIBUTE_NMTOKEN = 7;
+const XML_ATTRIBUTE_NMTOKENS = 8;
+const XML_ATTRIBUTE_NODE = 2;
+const XML_ATTRIBUTE_NOTATION = 10;
+const XML_CDATA_SECTION_NODE = 4;
+const XML_COMMENT_NODE = 8;
+const XML_DOCUMENT_FRAG_NODE = 11;
+const XML_DOCUMENT_NODE = 9;
+const XML_DOCUMENT_TYPE_NODE = 10;
+const XML_DTD_NODE = 14;
+const XML_ELEMENT_DECL_NODE = 15;
+const XML_ELEMENT_NODE = 1;
+const XML_ENTITY_DECL_NODE = 17;
+const XML_ENTITY_NODE = 6;
+const XML_ENTITY_REF_NODE = 5;
+const XML_HTML_DOCUMENT_NODE = 13;
+const XML_LOCAL_NAMESPACE = 18;
+const XML_NAMESPACE_DECL_NODE = 18;
+const XML_NOTATION_NODE = 12;
+const XML_PI_NODE = 7;
+const XML_TEXT_NODE = 3;
+}
*/
public function send( $recipients, array $headers, $body ) {
}
+ /**
+ * @return string
+ */
+ public function getMessage() {
+ }
}
class Mail_smtp extends Mail {
* Really delete the file
*
* @param Title &$title
- * @param File &$file
+ * @param LocalFile &$file
* @param string &$oldimage Archive name
* @param string $reason Reason of the deletion
* @param bool $suppress Whether to mark all deleted versions as restricted
if ( $oldimage ) {
$page = null;
$status = $file->deleteOld( $oldimage, $reason, $suppress, $user );
- if ( $status->ok ) {
+ if ( $status->isOK() ) {
// Need to do a log item
$logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
if ( trim( $reason ) != '' ) {
$wgOut->enableOOUI();
+ $fields = [];
+
+ $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
+ $this->prepareMessage( 'filedelete-intro' ) ) ]
+ );
+
$options = Xml::listDropDownOptions(
$wgOut->msg( 'filedelete-reason-dropdown' )->inContentLanguage()->text(),
[ 'other' => $wgOut->msg( 'filedelete-reason-otherlist' )->inContentLanguage()->text() ]
);
$options = Xml::listDropDownOptionsOoui( $options );
- $fields = [];
- $fields[] = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet(
- $this->prepareMessage( 'filedelete-intro' ) ) ]
- );
-
$fields[] = new OOUI\FieldLayout(
new OOUI\DropdownInputWidget( [
'name' => 'wpDeleteReasonList',
*/
public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
$db = wfGetDB( DB_REPLICA );
+ $like = [];
$target = $protocol . $filterEntry;
$bits = wfParseUrl( $target );
}
}
- $like = [];
$like[] = $bits['scheme'] . $bits['delimiter'] . $bits['host'];
if ( $subdomains ) {
namespace MediaWiki\Navigation;
-use MediaWiki\Linker\LinkTarget;
-use MessageLocalizer;
use Html;
+use MessageLocalizer;
+use Title;
/**
* Helper class for generating prev/next links for paging.
+ * @todo Use LinkTarget instead of Title
*
* @since 1.34
*/
*/
private $messageLocalizer;
+ /**
+ * @param MessageLocalizer $messageLocalizer
+ */
public function __construct( MessageLocalizer $messageLocalizer ) {
$this->messageLocalizer = $messageLocalizer;
}
/**
* Generate (prev x| next x) (20|50|100...) type links for paging
*
- * @param LinkTarget $title LinkTarget object to link
+ * @param Title $title Title object to link
* @param int $offset
* @param int $limit
* @param array $query Optional URL query parameter string
* @param bool $atend Optional param for specified if this is the last page
* @return string
*/
- public function buildPrevNextNavigation( LinkTarget $title, $offset, $limit,
- array $query = [], $atend = false
+ public function buildPrevNextNavigation(
+ Title $title,
+ $offset,
+ $limit,
+ array $query = [],
+ $atend = false
) {
# Make 'previous' link
$prev = $this->messageLocalizer->msg( 'prevn' )->title( $title )
# Make links to set number of items per page
$numLinks = [];
+ // @phan-suppress-next-next-line PhanUndeclaredMethod
+ // @fixme MessageLocalizer doesn't have a getLanguage() method!
$lang = $this->messageLocalizer->getLanguage();
foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
$numLinks[] = $this->numLink( $title, $offset, $num, $query,
/**
* Helper function for buildPrevNextNavigation() that generates links
*
- * @param LinkTarget $title LinkTarget object to link
+ * @param Title $title Title object to link
* @param int $offset
* @param int $limit
* @param array $query Extra query parameters
* @param string $class Value of the "class" attribute of the link
* @return string HTML fragment
*/
- private function numLink( LinkTarget $title, $offset, $limit, array $query, $link,
+ private function numLink( Title $title, $offset, $limit, array $query, $link,
$tooltipMsg, $class
) {
$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
* @param Title $t
*/
public function setTitle( Title $t ) {
+ // @phan-suppress-next-next-line PhanUndeclaredMethod
+ // @fixme Not all implementations of IContextSource have this method!
$this->getContext()->setTitle( $t );
}
$sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
$pieces = [];
- $pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes(
+ $htmlAttribs = Sanitizer::mergeAttributes(
$this->getRlClient()->getDocumentAttributes(),
$sk->getHtmlElementAttributes()
- ) );
+ );
+ $pieces[] = Html::htmlHeader( $htmlAttribs );
$pieces[] = Html::openElement( 'head' );
if ( $this->getHTMLTitle() == '' ) {
}
$pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
- $pieces[] = $this->getRlClient()->getHeadHtml();
+ $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null );
$pieces[] = $this->buildExemptModules();
$pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
$pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
*
* run() must be declared in the subclass. It cannot be declared as abstract
* here because it has a variable parameter list.
+ * @todo Declare it as abstract after dropping HHVM
*
* @package MediaWiki\Rest
*/
class SimpleHandler extends Handler {
public function execute() {
$params = array_values( $this->getRequest()->getPathParams() );
+ // @phan-suppress-next-line PhanUndeclaredMethod
return $this->run( ...$params );
}
}
*
* @since 1.31
* @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord
+ * @property MutableRevisionSlots $mSlots
*/
class MutableRevisionRecord extends RevisionRecord {
$slots = new MutableRevisionSlots();
parent::__construct( $title, $slots, $dbDomain );
-
- $this->mSlots = $slots; // redundant, but nice for static analysis
}
/**
// TODO: introduce something like an UnsavedRevisionFactory service instead!
/** @var MutableRevisionRecord $rev */
$rev = $this->derivedDataUpdater->getRevision();
+ '@phan-var MutableRevisionRecord $rev';
$rev->setPageId( $title->getArticleID() );
// splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
/** @var MediaWikiTitleCodec $titleCodec */
$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
+ '@phan-var MediaWikiTitleCodec $titleCodec';
// MalformedTitleException can be thrown here
$parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
/**
* The TitleArray class only exists to provide the newFromResult method at pre-
* sent.
+ *
+ * @method int count()
*/
abstract class TitleArray implements Iterator {
/**
use MediaWiki\Session\Session;
use MediaWiki\Session\SessionId;
use MediaWiki\Session\SessionManager;
+use Wikimedia\AtEase\AtEase;
// The point of this class is to be a wrapper around super globals
// phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
* @return array Any query arguments found in path matches.
*/
public static function getPathInfo( $want = 'all' ) {
- global $wgUsePathInfo;
// PATH_INFO is mangled due to https://bugs.php.net/bug.php?id=31892
// And also by Apache 2.x, double slashes are converted to single slashes.
// So we will use REQUEST_URI if possible.
- $matches = [];
- if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) {
// Slurp out the path portion to examine...
$url = $_SERVER['REQUEST_URI'];
if ( !preg_match( '!^https?://!', $url ) ) {
$url = 'http://unused' . $url;
}
- Wikimedia\suppressWarnings();
+ AtEase::suppressWarnings();
$a = parse_url( $url );
- Wikimedia\restoreWarnings();
- if ( $a ) {
- $path = $a['path'] ?? '';
-
- global $wgScript;
- if ( $path == $wgScript && $want !== 'all' ) {
- // Script inside a rewrite path?
- // Abort to keep from breaking...
- return $matches;
- }
+ AtEase::restoreWarnings();
+ if ( !$a ) {
+ return [];
+ }
+ $path = $a['path'] ?? '';
- $router = new PathRouter;
+ global $wgScript;
+ if ( $path == $wgScript && $want !== 'all' ) {
+ // Script inside a rewrite path?
+ // Abort to keep from breaking...
+ return [];
+ }
- // Raw PATH_INFO style
- $router->add( "$wgScript/$1" );
+ $router = new PathRouter;
- if ( isset( $_SERVER['SCRIPT_NAME'] )
- && preg_match( '/\.php/', $_SERVER['SCRIPT_NAME'] )
- ) {
- # Check for SCRIPT_NAME, we handle index.php explicitly
- # But we do have some other .php files such as img_auth.php
- # Don't let root article paths clober the parsing for them
- $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
- }
-
- global $wgArticlePath;
- if ( $wgArticlePath ) {
- $router->add( $wgArticlePath );
- }
+ // Raw PATH_INFO style
+ $router->add( "$wgScript/$1" );
- global $wgActionPaths;
- if ( $wgActionPaths ) {
- $router->add( $wgActionPaths, [ 'action' => '$key' ] );
- }
+ if ( isset( $_SERVER['SCRIPT_NAME'] )
+ && strpos( $_SERVER['SCRIPT_NAME'], '.php' ) !== false
+ ) {
+ // Check for SCRIPT_NAME, we handle index.php explicitly
+ // But we do have some other .php files such as img_auth.php
+ // Don't let root article paths clober the parsing for them
+ $router->add( $_SERVER['SCRIPT_NAME'] . "/$1" );
+ }
- global $wgVariantArticlePath;
- if ( $wgVariantArticlePath ) {
- $router->add( $wgVariantArticlePath,
- [ 'variant' => '$2' ],
- [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()->
- getVariants() ]
- );
- }
+ global $wgArticlePath;
+ if ( $wgArticlePath ) {
+ $router->add( $wgArticlePath );
+ }
- Hooks::run( 'WebRequestPathInfoRouter', [ $router ] );
+ global $wgActionPaths;
+ if ( $wgActionPaths ) {
+ $router->add( $wgActionPaths, [ 'action' => '$key' ] );
+ }
- $matches = $router->parse( $path );
+ global $wgVariantArticlePath;
+ if ( $wgVariantArticlePath ) {
+ $router->add( $wgVariantArticlePath,
+ [ 'variant' => '$2' ],
+ [ '$2' => MediaWikiServices::getInstance()->getContentLanguage()->
+ getVariants() ]
+ );
}
- } elseif ( $wgUsePathInfo ) {
- if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) {
- // Mangled PATH_INFO
- // https://bugs.php.net/bug.php?id=31892
- // Also reported when ini_get('cgi.fix_pathinfo')==false
- $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
-
- } elseif ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
- // Regular old PATH_INFO yay
- $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+
+ Hooks::run( 'WebRequestPathInfoRouter', [ $router ] );
+
+ $matches = $router->parse( $path );
+ } else {
+ global $wgUsePathInfo;
+ $matches = [];
+ if ( $wgUsePathInfo ) {
+ if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) ) {
+ // Mangled PATH_INFO
+ // https://bugs.php.net/bug.php?id=31892
+ // Also reported when ini_get('cgi.fix_pathinfo')==false
+ $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 );
+ } elseif ( !empty( $_SERVER['PATH_INFO'] ) ) {
+ // Regular old PATH_INFO yay
+ $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 );
+ }
}
}
$this->useTransactionalTimeLimit();
$old = $this->getRequest()->getText( 'oldimage' );
+ /** @var LocalFile $localFile */
$localFile = $this->page->getFile();
+ '@phan-var LocalFile $localFile';
$oldFile = OldLocalFile::newFromArchiveName( $this->getTitle(), $localFile->getRepo(), $old );
$source = $localFile->getArchiveVirtualUrl( $old );
$this->dieStatus( $this->errorArrayToStatus( $retval ) );
}
- list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
$res = [];
+
$res['user'] = $params['user'];
+ list( $target, /*...*/ ) = SpecialBlock::getTargetAndType( $params['user'] );
$res['userID'] = $target instanceof User ? $target->getId() : 0;
$block = DatabaseBlock::newFromTarget( $target, null, true );
$pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
$titleObj = $pageObj->getTitle();
if ( !$pageObj->exists() &&
+ // @phan-suppress-next-line PhanUndeclaredMethod
!( $titleObj->getNamespace() == NS_FILE && self::canDeleteFile( $pageObj->getFile() ) )
) {
$this->dieWithError( 'apierror-missingtitle' );
) {
$title = $page->getTitle();
+ // @phan-suppress-next-line PhanUndeclaredMethod There's no right typehint for it
$file = $page->getFile();
if ( !self::canDeleteFile( $file ) ) {
return self::delete( $page, $user, $reason, $tags );
* ApiResult.
* @since 1.25
* @ingroup API
+ * @phan-file-suppress PhanUndeclaredMethod Undeclared methods in IApiMessage
*/
class ApiErrorFormatter {
/** @var Title Dummy title to silence warnings from MessageCache::parse() */
$parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
$dom = $parser->preprocessToDom( $params['text'] );
if ( is_callable( [ $dom, 'saveXML' ] ) ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->saveXML();
} else {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->__toString();
}
if ( isset( $prop['parsetree'] ) ) {
if ( $e instanceof ApiUsageException ) {
foreach ( $e->getStatusValue()->getErrors() as $error ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$msg = ApiMessage::create( $error )
->inLanguage( $this->getLanguage() );
$errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() );
$tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
->newTempFSFile( 'rotate_', $ext );
$dstPath = $tmpFile->getPath();
+ // @phan-suppress-next-line PhanUndeclaredMethod
$err = $handler->rotate( $file, [
'srcPath' => $srcPath,
'dstPath' => $dstPath,
$comment = wfMessage(
'rotate-comment'
)->numParams( $rotation )->inContentLanguage()->text();
+ // @phan-suppress-next-line PhanUndeclaredMethod
$status = $file->upload(
$dstPath,
$comment,
* @param IContextSource|WebRequest|null $context If this is an instance of
* FauxRequest, errors are thrown and no printing occurs
* @param bool $enableWrite Should be set to true if the api may modify data
+ * @suppress PhanUndeclaredMethod
*/
public function __construct( $context = null, $enableWrite = false ) {
if ( $context === null ) {
* @since 1.27
* @ingroup API
* @phan-file-suppress PhanTraitParentReference
+ * @phan-file-suppress PhanUndeclaredMethod
*/
trait ApiMessageTrait {
case 'xml':
$printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
+ '@phan-var ApiFormatXML $printer';
$printer->setRootElement( 'SearchSuggestion' );
return $printer;
* @param string $search the search query
* @param array $params api request params
* @return array search results. Keys are integers.
- * @phan-return array<array{title:Title,extract:false,image:false,url:string}>
+ * @phan-return array<array{title:Title,redirect_from:?Title,extract:false,extract_trimmed:false,image:false,url:string}>
* Note that phan annotations don't support keys containing a space.
*/
private function search( $search, array $params ) {
$parser = MediaWikiServices::getInstance()->getParser();
$parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
$result_array['parsetree'] = $xml;
$result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
// Filter modules based on continue parameter
$continuationManager = new ApiContinuationManager( $this, $allModules, $propModules );
$this->setContinuationManager( $continuationManager );
+ /** @var ApiQueryBase[] $modules */
$modules = $continuationManager->getRunModules();
+ '@phan-var ApiQueryBase[] $modules';
if ( !$continuationManager->isGeneratorDone() ) {
// Query modules may optimize data requests through the $this->getPageSet()
$cacheMode = $this->mPageSet->getCacheMode();
// Execute all unfinished modules
- /** @var ApiQueryBase $module */
foreach ( $modules as $module ) {
$params = $module->extractRequestParams();
$cacheMode = $this->mergeCacheMode(
$id = $restriction->getBlockId();
switch ( $restriction->getType() ) {
case 'page':
+ /** @var \MediaWiki\Block\Restriction\PageRestriction $restriction */
+ '@phan-var \MediaWiki\Block\Restriction\PageRestriction $restriction';
$value = [ 'id' => $restriction->getValue() ];
if ( $restriction->getTitle() ) {
self::addTitleInfo( $value, $restriction->getTitle() );
$vals['thumbmime'] = $mime;
}
} elseif ( $mto && $mto->isError() ) {
+ /** @var MediaTransformError $mto */
+ '@phan-var MediaTransformError $mto';
$vals['thumberror'] = $mto->toText();
}
}
// Thus there should be no issue with format=xml.
$format = new FormatMetadata;
$format->setSingleLanguage( !$opts['multilang'] );
+ // @phan-suppress-next-line PhanUndeclaredMethod
$format->getContext()->setLanguage( $opts['language'] );
$extmetaArray = $format->fetchExtendedMetadata( $file );
if ( $opts['extmetadatafilter'] ) {
}
if ( $archive && $file->isOld() ) {
+ /** @var OldLocalFile $file */
+ '@phan-var OldLocalFile $file';
$vals['archivename'] = $file->getArchiveName();
}
if ( $this->fld_parsetree || ( $this->fld_content && $this->generateXML ) ) {
if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+ /** @var WikitextContent $content */
+ '@phan-var WikitextContent $content';
$t = $content->getText(); # note: don't set $text
$parser = MediaWikiServices::getInstance()->getParser();
);
$dom = $parser->preprocessToDom( $t );
if ( is_callable( [ $dom, 'saveXML' ] ) ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->saveXML();
} else {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->__toString();
}
$vals['parsetree'] = $xml;
if ( $this->expandTemplates && !$this->parseContent ) {
if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+ /** @var WikitextContent $content */
+ '@phan-var WikitextContent $content';
$text = $content->getText();
$text = MediaWikiServices::getInstance()->getParser()->preprocess(
}
}
- $result = [];
// No errors, no warnings: do the upload
+ $result = [];
if ( $this->mParams['async'] ) {
$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
if ( $progress && $progress['result'] === 'Poll' ) {
public function __construct() {
/** @var SessionProvider $provider */
$provider = SessionManager::getGlobalSession()->getProvider();
+ '@phan-var SessionProvider $provider';
$this->expiration = $provider->getRememberUserDuration();
}
if ( $block instanceof SystemBlock ) {
$systemBlocks[] = $block;
} elseif ( $block->getType() === DatabaseBlock::TYPE_AUTO ) {
+ /** @var DatabaseBlock $block */
+ '@phan-var DatabaseBlock $block';
if ( !isset( $databaseBlocks[$block->getParentBlockId()] ) ) {
$databaseBlocks[$block->getParentBlockId()] = $block;
}
* @inheritDoc
*/
public static function newFromRow( \stdClass $row ) {
+ /** @var self $restriction */
$restriction = parent::newFromRow( $row );
+ '@phan-var self $restriction';
// If the page_namespace and the page_title were provided, add the title to
// the restriction.
*
* @since 1.33
* @param \stdClass $row
- * @return self
+ * @return static
*/
public static function newFromRow( \stdClass $row );
* but 'Bot' is unchecked, hidebots=1 will be sent.
*
* @since 1.29
+ * @method ChangesListBooleanFilter[] getFilters()
*/
class ChangesListBooleanFilterGroup extends ChangesListFilterGroup {
/**
* Registers a filter in this group
*
* @param ChangesListBooleanFilter $filter
+ * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch
*/
public function registerFilter( ChangesListBooleanFilter $filter ) {
$this->filters[$filter->getName()] = $filter;
* Represents a filter group (used on ChangesListSpecialPage and descendants)
*
* @since 1.29
+ * @method registerFilter($filter)
*/
abstract class ChangesListFilterGroup {
/**
* Registers a filter in this group
*
* @param ChangesListStringOptionsFilter $filter
+ * @suppress PhanParamSignaturePHPDocMismatchHasParamType,PhanParamSignatureMismatch
*/
public function registerFilter( ChangesListStringOptionsFilter $filter ) {
$this->filters[$filter->getName()] = $filter;
/**
* Generic list for change tagging.
+ *
+ * @property ChangeTagsLogItem $current
+ * @method ChangeTagsLogItem next()
+ * @method ChangeTagsLogItem reset()
+ * @method ChangeTagsLogItem current()
+ * @phan-file-suppress PhanParamSignatureMismatch
*/
abstract class ChangeTagsList extends RevisionListBase {
function __construct( IContextSource $context, Title $title, array $ids ) {
* @return string|bool The raw text, or false if the conversion failed.
*/
public function getWikitextForTransclusion() {
+ /** @var WikitextContent $wikitext */
$wikitext = $this->convert( CONTENT_MODEL_WIKITEXT, 'lossy' );
+ '@phan-var WikitextContent $wikitext';
if ( $wikitext ) {
return $wikitext->getText();
*/
public function diff( Content $that, Language $lang = null ) {
$this->checkModelID( $that->getModel() );
-
+ /** @var self $that */
+ '@phan-var self $that';
// @todo could implement this in DifferenceEngine and just delegate here?
if ( !$lang ) {
public function serializeContent( Content $content, $format = null ) {
$this->checkFormat( $format );
+ // @phan-suppress-next-line PhanUndeclaredMethod
return $content->getText();
}
*/
public function serializeContent( Content $content, $format = null ) {
/** @var UnknownContent $content */
+ '@phan-var UnknownContent $content';
return $content->getData();
}
"document uses $myModelId but " .
"section uses $sectionModelId." );
}
+ /** @var self $with $oldtext */
+ '@phan-var self $with';
$oldtext = $this->getText();
$text = $with->getText();
if ( isset( $queue[$class] ) ) {
/** @var MergeableUpdate $existingUpdate */
$existingUpdate = $queue[$class];
+ '@phan-var MergeableUpdate $existingUpdate';
$existingUpdate->merge( $update );
// Move the update to the end to handle things like mergeable purge
// updates that might depend on the prior updates in the queue running
/** @var TextSlotDiffRenderer $slotDiffRenderer */
$slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
->getSlotDiffRenderer( RequestContext::getMain() );
+ '@phan-var TextSlotDiffRenderer $slotDiffRenderer';
return $slotDiffRenderer->getTextDiff( $oldText, $newText );
}
use MediaWiki\MediaWikiServices as MediaWikiServicesAlias;
use MediaWiki\Storage\RevisionRecord;
use Wikimedia\Rdbms\IResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
/**
* @ingroup SpecialPage Dump
/** @var XmlDumpWriter */
private $writer;
- /** @var IDatabase */
+ /** @var Database */
protected $db;
/** @var array|int */
}
/**
- * @param IDatabase $db
+ * @param Database $db
* @param int|array $history One of WikiExporter::FULL, WikiExporter::CURRENT,
* WikiExporter::RANGE or WikiExporter::STABLE, or an associative array:
* - offset: non-inclusive offset at which to start the query
*/
function writeUpload( $file, $dumpContents = false ) {
if ( $file->isOld() ) {
+ /** @var OldLocalFile $file */
+ '@phan-var OldLocalFile $file';
$archiveName = " " .
Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
} else {
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBError;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* Version of FileJournal that logs to a DB table
protected $domain;
/**
- * Construct a new instance from configuration.
+ * Construct a new instance from configuration. Do not call directly, use FileJournal::factory.
*
* @param array $config Includes:
* domain: database domain ID of the wiki
*/
- protected function __construct( array $config ) {
+ public function __construct( array $config ) {
parent::__construct( $config );
$this->domain = $config['domain'] ?? $config['wiki']; // b/c
return $status;
}
- $now = wfTimestamp( TS_UNIX );
+ $now = ConvertibleTimestamp::time();
$data = [];
foreach ( $entries as $entry ) {
try {
$dbw->insert( 'filejournal', $data, __METHOD__ );
+ // XXX Should we do this deterministically so it's testable? Maybe look at the last two
+ // digits of a hash of a bunch of the data?
if ( mt_rand( 0, 99 ) == 0 ) {
- $this->purgeOldLogs(); // occasionally delete old logs
+ // occasionally delete old logs
+ $this->purgeOldLogs(); // @codeCoverageIgnore
}
} catch ( DBError $e ) {
$status->fatal( 'filejournal-fail-dbquery', $this->backend );
}
$dbw = $this->getMasterDB();
- $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays );
+ $dbCutoff = $dbw->timestamp( ConvertibleTimestamp::time() - 86400 * $this->ttlDays );
$dbw->delete( 'filejournal',
[ 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ],
* @return string
*/
public static function getHashFromKey( $key ) {
- return strtok( $key, '.' );
+ $sha1 = strtok( $key, '.' );
+ if ( is_string( $sha1 ) && strlen( $sha1 ) === 32 && $sha1[0] === '0' ) {
+ $sha1 = substr( $sha1, 1 );
+ }
+ return $sha1;
}
/**
$thumb = false;
} elseif ( $thumb->isError() ) { // transform error
/** @var MediaTransformError $thumb */
+ '@phan-var MediaTransformError $thumb';
$this->lastError = $thumb->toText();
// Ignore errors if requested
if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
: FSFile::getSha1Base36FromPath( $srcPath );
/** @var FileBackendDBRepoWrapper $wrapperBackend */
$wrapperBackend = $repo->getBackend();
+ '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
$dst = $wrapperBackend->getPathForSHA1( $sha1 );
$status = $repo->quickImport( $src, $dst );
if ( $flags & File::DELETE_SOURCE ) {
$oldTitleFile->purgeEverything();
foreach ( $archiveNames as $archiveName ) {
/** @var OldLocalFile $oldTitleFile */
+ '@phan-var OldLocalFile $oldTitleFile';
$oldTitleFile->purgeOldThumbnails( $archiveName );
}
$newTitleFile->purgeEverything();
public function execute() {
$repo = $this->file->repo;
$status = $repo->newGood();
+ /** @var LocalFile $destFile */
$destFile = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
->newFile( $this->target );
+ '@phan-var LocalFile $destFile';
$this->file->lock();
$destFile->lock(); // quickly fail if destination is not available
* (defined in htmlform.Element.js) picks up the extra config when constructed using OO.ui.infuse().
*
* Currently only supports passing 'hide-if' data.
+ * @phan-file-suppress PhanUndeclaredMethod
*/
trait HTMLFormElement {
/**
* Take care of whatever is necessary to perform the URI request.
*
- * @return StatusValue
+ * @return Status
* @note currently returns Status for B/C
*/
public function execute() {
$user
);
} else {
+ '@phan-var LocalFile $file';
$flags = 0;
$status = $file->upload(
$source,
* This will return a cached connection if one is available.
*
* @return Status
+ * @suppress PhanUndeclaredMethod
*/
public function getConnection() {
if ( $this->db ) {
public function setupSchemaVars() {
$status = $this->getConnection();
if ( $status->isOK() ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$status->value->setSchemaVars( $this->getSchemaVars() );
} else {
$msg = __METHOD__ . ': unexpected error while establishing'
$cl = $this->maintenance->runChild(
RebuildLocalisationCache::class, 'rebuildLocalisationCache.php'
);
+ '@phan-var RebuildLocalisationCache $cl';
$this->output( "Rebuilding localisation cache...\n" );
$cl->setForce();
$cl->execute();
$task = $this->maintenance->runChild(
MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php'
);
+ // @phan-suppress-next-line PhanUndeclaredMethod
$task->setForce();
$ok = $task->execute();
$this->output( $ok ? "done.\n" : "errors were encountered.\n" );
if ( $this->db->fieldExists( 'archive', 'ar_text', __METHOD__ ) ) {
$this->output( "Migrating archive ar_text to modern storage.\n" );
$task = $this->maintenance->runChild( MigrateArchiveText::class, 'migrateArchiveText.php' );
+ // @phan-suppress-next-line PhanUndeclaredMethod
$task->setForce();
if ( $task->execute() ) {
$this->applyPatch( 'patch-drop-ar_text.sql', false,
if ( !$status->isOK() ) {
return $status;
}
+ // @phan-suppress-next-line PhanUndeclaredMethod
$status->value->insert(
'site_stats',
[
* @var Database $conn
*/
$conn = $status->value;
+ '@phan-var Database $conn';
// Check version
return static::meetsMinimumRequirement( $conn->getServerVersion() );
*
* @ingroup Deployment
* @since 1.17
+ * @property DatabaseMysqlBase $db
*/
class MysqlUpdater extends DatabaseUpdater {
protected function getCoreUpdateList() {
if ( !$status->isOK() ) {
return $status;
}
+ // @phan-suppress-next-line PhanUndeclaredMethod
$exists = $status->value->roleExists( $this->getVar( 'wgDBuser' ) );
}
}
/** @var DatabasePostgres $conn */
$conn = $status->value;
+ '@phan-var DatabasePostgres $conn';
// Create the schema if necessary
$schema = $this->getVar( 'wgDBmwschema' );
if ( !$status->isOK() ) {
return $status;
}
+ /** @var DatabasePostgres $conn */
$conn = $status->value;
+ '@phan-var DatabasePostgres $conn';
$safeuser = $conn->addIdentifierQuotes( $this->getVar( 'wgDBuser' ) );
$safepass = $conn->addQuotes( $this->getVar( 'wgDBpassword' ) );
/** @var DatabasePostgres $conn */
$conn = $status->value;
+ '@phan-var DatabasePostgres $conn';
if ( $conn->tableExists( 'archive' ) ) {
$status->warning( 'config-install-tables-exist' );
# Special case for Creative Commons partner chooser box.
if ( $this->request->getVal( 'SubmitCC' ) ) {
+ /** @var WebInstallerOptions $page */
$page = $this->getPageByName( 'Options' );
+ '@phan-var WebInstallerOptions $page';
$this->output->useShortHeader();
$this->output->allowFrames();
$page->submitCC();
}
if ( $this->request->getVal( 'ShowCC' ) ) {
+ /** @var WebInstallerOptions $page */
$page = $this->getPageByName( 'Options' );
+ '@phan-var WebInstallerOptions $page';
$this->output->useShortHeader();
$this->output->allowFrames();
$this->output->addHTML( $page->getCCDoneBox() );
foreach ( $moduleNames as $moduleName ) {
/** @var ResourceLoaderFileModule $module */
$module = $resourceLoader->getModule( $moduleName );
+ '@phan-var ResourceLoaderFileModule $module';
if ( !$module ) {
// T98043: Don't fatal, but it won't look as pretty.
continue;
abstract class FileJournal {
/** @var string */
protected $backend;
- /** @var int */
+ /** @var int|false */
protected $ttlDays;
/**
* A starting change ID and/or limit can be specified.
*
* @param int|null $start Starting change ID or null
- * @param int $limit Maximum number of items to return
+ * @param int $limit Maximum number of items to return (0 = unlimited)
* @param string|null &$next Updated to the ID of the next entry.
* @return array List of associative arrays, each having:
* id : unique, monotonic, ID for this change
$outputPage->enableOOUI();
+ $fields = [];
+
$options = Xml::listDropDownOptions(
$ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->text(),
[ 'other' => $ctx->msg( 'deletereasonotherlist' )->inContentLanguage()->text() ]
);
$options = Xml::listDropDownOptionsOoui( $options );
- $fields = [];
$fields[] = new OOUI\FieldLayout(
new OOUI\DropdownInputWidget( [
'name' => 'wpDeleteReasonList',
$lang = $this->getLanguage();
$pm = MediaWikiServices::getInstance()->getPermissionManager();
$timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
+ // @phan-suppress-next-line PhanUndeclaredMethod
$img = $iscur ? $file->getName() : $file->getArchiveName();
$userId = $file->getUser( 'id' );
$userText = $file->getUser( 'text' );
* @ingroup Media
*
* @property WikiFilePage $mPage Set by overwritten newPage() in this class
+ * @method WikiFilePage getPage()
*/
class ImagePage extends Article {
/** @var File|false */
/**
* Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
+ *
+ * @method array getActionOverrides()
+ * @method string getUserText($audience=1,User $user=null)
+ * @method string getTimestamp()
+ * @method Title getTitle()
*/
interface Page {
}
/**
* @ingroup Parser
+ * @property string[] $out
*/
// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPDPart_Hash extends PPDPart {
* @ingroup Parser
*/
class PPDStack {
- public $stack, $rootAccum;
+ /** @var PPDStackElement[] */
+ public $stack;
+ public $rootAccum;
/**
- * @var PPDStack|false
+ * @var PPDStackElement|false
*/
public $top;
public $out;
/**
* @ingroup Parser
+ * @property PPDPart_Hash[] $parts
*/
// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPDStackElement_Hash extends PPDStackElement {
*/
public function breakSyntax( $openingCount = false ) {
if ( $this->open == "\n" ) {
- // @phan-suppress-next-line PhanTypeMismatchArgumentInternal
$accum = array_merge( [ $this->savedPrefix ], $this->parts[0]->out );
} else {
if ( $openingCount === false ) {
} else {
$accum[++$lastIndex] = '|';
}
- // @phan-suppress-next-line PhanTypeMismatchForeach
+
foreach ( $part->out as $node ) {
if ( is_string( $node ) && is_string( $accum[$lastIndex] ) ) {
$accum[$lastIndex] .= $node;
* An expansion frame, used as a context to expand the result of preprocessToObj()
* @deprecated since 1.34, use PPFrame_Hash
* @ingroup Parser
+ * @phan-file-suppress PhanUndeclaredMethod
*/
// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPFrame_DOM implements PPFrame {
/**
* @deprecated since 1.34, use PPNode_Hash_{Tree,Text,Array,Attr}
* @ingroup Parser
+ * @phan-file-suppress PhanUndeclaredMethod
*/
// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
class PPNode_DOM implements PPNode {
*/
private static function normalizeSectionName( $text ) {
# T90902: ensure the same normalization is applied for IDs as to links
+ /** @var MediaWikiTitleCodec $titleParser */
$titleParser = MediaWikiServices::getInstance()->getTitleParser();
+ '@phan-var MediaWikiTitleCodec $titleParser';
try {
$parts = $titleParser->splitTitleString( "#$text" );
}
$i += $count;
} elseif ( $found == 'close' ) {
+ /** @var PPDStackElement_Hash $piece */
$piece = $stack->top;
+ '@phan-var PPDStackElement_Hash $piece';
# lets check if there are enough characters for closing brace
$maxCount = $piece->count;
if ( $piece->close === '}-' && $curChar === '}' ) {
// Construct pseudo-hash based on params and arguments
/** @var ParameterizedPassword $passObj */
$passObj = $this->factory->newFromType( $type );
+ '@phan-var ParameterizedPassword $passObj';
$params = '';
$args = '';
// Hash the last hash with the next type in the layer
$passObj = $this->factory->newFromCiphertext( $existingHash );
+ '@phan-var ParameterizedPassword $passObj';
$passObj->crypt( $lastHash );
// Move over the params and args
// Construct pseudo-hash based on params and arguments
/** @var ParameterizedPassword $passObj */
$passObj = $this->factory->newFromType( $type );
+ '@phan-var ParameterizedPassword $passObj';
$params = '';
$args = '';
// Hash the last hash with the next type in the layer
$passObj = $this->factory->newFromCiphertext( $existingHash );
+ '@phan-var ParameterizedPassword $passObj';
$passObj->crypt( $lastHash );
// Move over the params and args
if ( !$status->isOK() ) {
return $status;
}
+ /** @var RedisConnRef $conn */
$conn = $status->value;
+ '@phan-var RedisConnRef $conn';
// phpcs:disable Generic.Files.LineLength
static $script =
if ( !$status->isOK() ) {
return $status;
}
+ /** @var RedisConnRef $conn */
$conn = $status->value;
+ '@phan-var RedisConnRef $conn';
$now = microtime( true );
try {
* Handle the form submission if everything validated properly
*
* @param array $formData
- * @param HTMLForm $form
+ * @param PreferencesFormOOUI $form
* @param array[] $formDescriptor
* @return bool|Status|string
*/
- protected function saveFormData( $formData, HTMLForm $form, array $formDescriptor ) {
- /** @var \User $user */
+ protected function saveFormData( $formData, PreferencesFormOOUI $form, array $formDescriptor ) {
$user = $form->getModifiedUser();
$hiddenPrefs = $this->options->get( 'HiddenPrefs' );
$result = true;
* Save the form data and reload the page
*
* @param array $formData
- * @param HTMLForm $form
+ * @param PreferencesFormOOUI $form
* @param array $formDescriptor
* @return Status
*/
- protected function submitForm( array $formData, HTMLForm $form, array $formDescriptor ) {
+ protected function submitForm(
+ array $formData,
+ PreferencesFormOOUI $form,
+ array $formDescriptor
+ ) {
$res = $this->saveFormData( $formData, $form, $formDescriptor );
if ( $res === true ) {
* @ingroup Profiler
*
* @since 1.25
+ * @property ProfilerXhprof $collector
*/
class ProfilerOutputDump extends ProfilerOutput {
}
/**
+ * @internal For use by ResourceLoaderStartUpModule only.
+ */
+ const HASH_LENGTH = 5;
+
+ /**
+ * Create a hash for module versioning purposes.
+ *
+ * This hash is used in three ways:
+ *
+ * - To differentiate between the current version and a past version
+ * of a module by the same name.
+ *
+ * In the cache key of localStorage in the browser (mw.loader.store).
+ * This store keeps only one version of any given module. As long as the
+ * next version the client encounters has a different hash from the last
+ * version it saw, it will correctly discard it in favour of a network fetch.
+ *
+ * A browser may evict a site's storage container for any reason (e.g. when
+ * the user hasn't visited a site for some time, and/or when the device is
+ * low on storage space). Anecdotally it seems devices rarely keep unused
+ * storage beyond 2 weeks on mobile devices and 4 weeks on desktop.
+ * But, there is no hard limit or expiration on localStorage.
+ * ResourceLoader's Client also clears localStorage when the user changes
+ * their language preference or when they (temporarily) use Debug Mode.
+ *
+ * The only hard factors that reduce the range of possible versions are
+ * 1) the name and existence of a given module, and
+ * 2) the TTL for mw.loader.store, and
+ * 3) the `$wgResourceLoaderStorageVersion` configuration variable.
+ *
+ * - To identify a batch response of modules from load.php in an HTTP cache.
+ *
+ * When fetching modules in a batch from load.php, a combined hash
+ * is created by the JS code, and appended as query parameter.
+ *
+ * In cache proxies (e.g. Varnish, Nginx) and in the browser's HTTP cache,
+ * these urls are used to identify other previously cached responses.
+ * The range of possible versions a given version has to be unique amongst
+ * is determined by the maximum duration each response is stored for, which
+ * is controlled by `$wgResourceLoaderMaxage['versioned']`.
+ *
+ * - To detect race conditions between multiple web servers in a MediaWiki
+ * deployment of which some have the newer version and some still the older
+ * version.
+ *
+ * An HTTP request from a browser for the Startup manifest may be responded
+ * to by a server with the newer version. The browser may then use that to
+ * request a given module, which may then be responded to by a server with
+ * the older version. To avoid caching this for too long (which would pollute
+ * all other users without repairing itself), the combined hash that the JS
+ * client adds to the url is verified by the server (in ::sendResponseHeaders).
+ * If they don't match, we instruct cache proxies and clients to not cache
+ * this response as long as they normally would. This is also the reason
+ * that the algorithm used here in PHP must match the one used in JS.
+ *
+ * The fnv132 digest creates a 32-bit integer, which goes upto 4 Giga and
+ * needs up to 7 chars in base 36.
+ * Within 7 characters, base 36 can count up to 78,364,164,096 (78 Giga),
+ * (but with fnv132 we'd use very little of this range, mostly padding).
+ * Within 6 characters, base 36 can count up to 2,176,782,336 (2 Giga).
+ * Within 5 characters, base 36 can count up to 60,466,176 (60 Mega).
+ *
* @since 1.26
* @param string $value
* @return string Hash
*/
public static function makeHash( $value ) {
$hash = hash( 'fnv132', $value );
- return Wikimedia\base_convert( $hash, 16, 36, 7 );
+ // The base_convert will pad it (if too short),
+ // then substr() will trim it (if too long).
+ return substr(
+ Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
+ 0,
+ self::HASH_LENGTH
+ );
}
/**
* - Inline scripts can't be asynchronous.
* - For styles, earlier is better.
*
+ * @param string|null $nojsClass Class name that caller uses on HTML document element
* @return string|WrappedStringList HTML
*/
- public function getHeadHtml() {
+ public function getHeadHtml( $nojsClass = null ) {
$nonce = $this->options['nonce'];
$data = $this->getData();
$chunks = [];
// Change "client-nojs" class to client-js. This allows easy toggling of UI components.
// This must happen synchronously on every page view to avoid flashes of wrong content.
- // See also #getDocumentAttributes() and /resources/src/startup.js.
- $script = <<<'JAVASCRIPT'
-document.documentElement.className = document.documentElement.className
- .replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );
+ // See also startup/startup.js.
+ $nojsClass = $nojsClass ?? $this->getDocumentAttributes()['class'];
+ $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass );
+ $jsClassJson = ResourceLoader::encodeJsonForScript( $jsClass );
+ $script = <<<JAVASCRIPT
+document.documentElement.className = {$jsClassJson};
JAVASCRIPT;
// Inline script: Declare mw.config variables for this page.
'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
'wgSiteName' => $conf->get( 'Sitename' ),
'wgDBname' => $conf->get( 'DBname' ),
+ 'wgWikiID' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ),
'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
// MediaWiki sets cookies to have this prefix by default
$states[$name] = 'error';
}
- if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
+ if ( $versionHash !== '' && strlen( $versionHash ) !== ResourceLoader::HASH_LENGTH ) {
$e = new RuntimeException( "Badly formatted module version hash" );
$resourceLoader->outputErrorAndLog( $e,
"Module '{module}' produced an invalid version hash: '{version}'.",
/**
* Item class for a filearchive table row
+ *
+ * @property ArchivedFile $file
+ * @property RevDelArchivedFileList $list
*/
class RevDelArchivedFileItem extends RevDelFileItem {
- /** @var RevDelArchivedFileList $list */
- /** @var ArchivedFile $file */
/** @var LocalFile */
protected $lockFile;
}
public function doPostCommitUpdates( array $visibilityChangeMap ) {
+ /** @var LocalFile $file */
$file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
->newFile( $this->title );
+ '@phan-var LocalFile $file';
$file->purgeCache();
$file->purgeDescription();
* needs to be able to make a query from a set of identifiers to pull
* relevant rows, to return RevDelItem subclasses wrapping them, and
* to wrap bulk update operations.
+ *
+ * @property RevDelItem $current
+ * @method RevDelItem next()
+ * @method RevDelItem reset()
+ * @method RevDelItem current()
+ * @phan-file-suppress PhanParamSignatureMismatch
*/
abstract class RevDelList extends RevisionListBase {
function __construct( IContextSource $context, Title $title, array $ids ) {
/**
* Item class for a live revision table row
+ *
+ * @property RevDelRevisionList $list
*/
class RevDelRevisionItem extends RevDelItem {
/** @var Revision */
* This trait can be used directly by extensions providing a SearchEngine.
*
* @ingroup Search
+ * @phan-file-suppress PhanUndeclaredMethod
*/
trait SearchResultSetTrait {
/**
/** @var array Track original session fields for later modification check */
protected $sessionFieldCache = [];
- protected function __construct( SessionManagerInterface $manager ) {
+ protected function __construct( SessionManager $manager ) {
$this->setEnableFlags(
\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
);
/**
* Install a session handler for the current web request
- * @param SessionManagerInterface $manager
+ * @param SessionManager $manager
*/
- public static function install( SessionManagerInterface $manager ) {
+ public static function install( SessionManager $manager ) {
if ( self::$instance ) {
$manager->setupPHPSessionHandler( self::$instance );
return;
$this->idIsSafe = $data['idIsSafe'];
$this->forceUse = $data['forceUse'] && $this->provider;
} else {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$this->id = $this->provider->getManager()->generateSessionId();
$this->idIsSafe = true;
$this->forceUse = false;
/**
* Get the global SessionManager
- * @return SessionManagerInterface
- * (really a SessionManager, but this is to make IDEs less confused)
+ * @return self
*/
public static function singleton() {
if ( self::$instance === null ) {
/** @var CreditsAction $action */
$action = Action::factory(
'credits', $this->getWikiPage(), $this->getContext() );
+ '@phan-var CreditsAction $action';
$tpl->set( 'credits',
$action->getCredits( $wgMaxCredits, $wgShowCreditsIfMax ) );
} else {
$isLoggedIn = $this->getUser()->isLoggedIn();
$continuePart = $this->isContinued() ? 'continue-' : '';
$anotherPart = $isLoggedIn ? 'another-' : '';
+ // @phan-suppress-next-line PhanUndeclaredMethod
$expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
$expirationDays = ceil( $expiration / ( 3600 * 24 ) );
$secureLoginLink = '';
return $title;
}
+ // @phan-suppress-next-line PhanUndeclaredMethod
$context->setTitle( $page->getPageTitle( $par ) );
} elseif ( !$page->isIncludable() ) {
return false;
foreach ( $block->getRestrictions() as $restriction ) {
switch ( $restriction->getType() ) {
case PageRestriction::TYPE:
+ /** @var PageRestriction $restriction */
+ '@phan-var PageRestriction $restriction';
if ( $restriction->getTitle() ) {
$pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
}
$dom = $parser->preprocessToDom( $input );
if ( method_exists( $dom, 'saveXML' ) ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->saveXML();
} else {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$xml = $dom->__toString();
}
}
$user = $this->getUser();
$significance = $this->getFilterGroup( 'significance' );
+ /** @var ChangesListBooleanFilter $hideMinor */
$hideMinor = $significance->getFilter( 'hideminor' );
+ '@phan-var ChangesListBooleanFilter $hideMinor';
$hideMinor->setDefault( $user->getBoolOption( 'hideminor' ) );
$automated = $this->getFilterGroup( 'automated' );
+ /** @var ChangesListBooleanFilter $hideBots */
$hideBots = $automated->getFilter( 'hidebots' );
+ '@phan-var ChangesListBooleanFilter $hideBots';
$hideBots->setDefault( true );
+ /** @var ChangesListStringOptionsFilterGroup|null $reviewStatus */
$reviewStatus = $this->getFilterGroup( 'reviewStatus' );
+ '@phan-var ChangesListStringOptionsFilterGroup|null $reviewStatus';
if ( $reviewStatus !== null ) {
// Conditional on feature being available and rights
if ( $user->getBoolOption( 'hidepatrolled' ) ) {
$reviewStatus->setDefault( 'unpatrolled' );
$legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
+ /** @var ChangesListBooleanFilter $legacyHidePatrolled */
$legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
+ '@phan-var ChangesListBooleanFilter $legacyHidePatrolled';
$legacyHidePatrolled->setDefault( true );
}
}
$changeType = $this->getFilterGroup( 'changeType' );
+ /** @var ChangesListBooleanFilter $hideCategorization */
$hideCategorization = $changeType->getFilter( 'hidecategorization' );
+ '@phan-var ChangesListBooleanFilter $hideCategorization';
if ( $hideCategorization !== null ) {
// Conditional on feature being available
$hideCategorization->setDefault( $user->getBoolOption( 'hidecategorization' ) );
$buttonFields = [];
if ( $isText ) {
+ '@phan-var TextContent $content';
// TODO: MCR: make this work for multiple slots
// source view for textual content
$sourceView = Xml::element( 'textarea', [
*
* @param string|null $par String if any subpage provided, else null
* @throws UserBlockedError|PermissionsError
+ * @suppress PhanUndeclaredMethod
*/
public function execute( $par ) {
$user = $this->getUser();
$this->getOutput()->addWikiTextAsInterface( $status->getWikiText() );
return;
- } else {
- $user = $status->value;
}
+ /** @var User $user */
+ $user = $status->value;
+ '@phan-var User $user';
+
$groups = $user->getGroups();
$groupMemberships = $user->getGroupMemberships();
$this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
/**
* @inheritDoc
+ * @suppress PhanUndeclaredMethod
*/
protected function registerFilters() {
parent::registerFilters();
switch ( $restriction->getType() ) {
case PageRestriction::TYPE:
+ '@phan-var PageRestriction $restriction';
if ( $restriction->getTitle() ) {
$items[$restriction->getType()][] = Html::rawElement(
'li',
$throttle->clear( $user->getName(), $request->getIP() );
}
return self::loginHook( $user, $bp,
+ // @phan-suppress-next-line PhanUndeclaredMethod
Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
}
* @return string|string[] An error or list of errors in the
* provided $datum. When no errors exist the empty array is
* returned.
+ * @suppress PhanUndeclaredMethod
*/
public static function getErrors( AvroSchema $schema, $datum ) {
switch ( $schema->type ) {
const SUPPORTED = 'mwfile';
/**
- * @var LanguageConverter
+ * @var LanguageConverter|FakeConverter
*/
public $mConverter;
}
function __construct() {
- // @phan-suppress-next-line PhanTypeMismatchProperty
$this->mConverter = new FakeConverter( $this );
// Set the code to the name of the descendant
if ( static::class === 'Language' ) {
$revision = Revision::newFromTitle( $title );
if ( $revision ) {
if ( $revision->getContentModel() == CONTENT_MODEL_WIKITEXT ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$txt = $revision->getContent( RevisionRecord::RAW )->getText();
}
$res = $dbw->select( 'content', 'content_address', [], __METHOD__, [ 'DISTINCT' ] );
$blobStore = MediaWikiServices::getInstance()->getBlobStore();
foreach ( $res as $row ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$textId = $blobStore->getTextIdFromAddress( $row->content_address );
if ( $textId ) {
$cur[] = $textId;
*/
public function execute() {
$siteStore = MediaWikiServices::getInstance()->getSiteStore();
- $siteStore->reset();
+ if ( method_exists( $siteStore, 'reset' ) ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
+ $siteStore->reset();
+ }
$globalId = $this->getArg( 0 );
$group = $this->getArg( 1 );
$siteStore->saveSites( [ $site ] );
if ( method_exists( $siteStore, 'reset' ) ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$siteStore->reset();
}
require __DIR__ . '/../commandLine.inc';
-use Wikimedia\Rdbms\IMaintainableDatabase;
-
/**
* Maintenance script that upgrade for log_id/log_deleted fields in a
* replication-safe way.
class UpdateLogging {
/**
- * @var IMaintainableDatabase
+ * @var Database
*/
public $dbw;
public $batchSize = 1000;
public $minTs = false;
function execute() {
- $this->dbw = $this->getDB( DB_MASTER );
+ $this->dbw = wfGetDB( DB_MASTER );
$logging = $this->dbw->tableName( 'logging' );
$logging_1_10 = $this->dbw->tableName( 'logging_1_10' );
$logging_pre_1_10 = $this->dbw->tableName( 'logging_pre_1_10' );
"$IP/tests/phpunit/phpunit.php",
"$IP/tests/phpunit/suites/LessTestSuite.php"
];
+ // @phan-suppress-next-line PhanUndeclaredMethod
$textUICommand->run( $argv );
}
}
protected function doOperations( FileRepo $tempRepo, array $ops ) {
$status = $tempRepo->getBackend()->doQuickOperations( $ops );
if ( !$status->isOK() ) {
+ // @phan-suppress-next-line PhanUndeclaredMethod
$this->error( print_r( $status->getErrorsArray(), true ) );
}
}
return;
}
+ /** @var WikitextContent $content */
+ '@phan-var WikitextContent $content';
$text = strval( $content->getText() );
$output1 = $parser1->parse( $text, $title, $this->options );
}
$res = $dbw->query( "SELECT l_from FROM $links LIMIT 1" );
+ // @phan-suppress-next-line PhanUndeclaredMethod
if ( $dbw->fieldType( $res, 0 ) == "int" ) {
$this->output( "Schema already converted\n" );
/** @var LocalFile $file */
$file = $repo->newFile( $row->fa_name );
+ '@phan-var LocalFile $file';
try {
$file->lock();
} catch ( LocalFileLockError $e ) {
$this->output( "Deleted version '$key' ($ts) of file '$name'\n" );
} else {
$this->output( "Failed to delete version '$key' ($ts) of file '$name'\n" );
+ // @phan-suppress-next-line PhanUndeclaredMethod
$this->output( print_r( $status->getErrorsArray(), true ) );
}
} else {
}
$this->uploadCount++;
// $this->report();
+ // @phan-suppress-next-line PhanUndeclaredMethod
$this->progress( "upload: " . $revision->getFilename() );
if ( !$this->dryRun ) {
if ( $this->hasOption( 'dry' ) ) {
$this->output( "done.\n" );
+ // @phan-suppress-next-line PhanUndeclaredMethod
} elseif ( $image->recordUpload2(
$archive->value,
$summary,
$dbw->query( "DELETE FROM $tbl_pag WHERE page_id = $id" );
$this->commitTransaction( $dbw, __METHOD__ );
// Delete revisions as appropriate
+ /** @var NukePage $child */
$child = $this->runChild( NukePage::class, 'nukePage.php' );
+ '@phan-var NukePage $child';
$child->deleteRevisions( $revs );
$this->purgeRedundantText( true );
$n_deleted++;
}
// Upgrade the old file versions...
foreach ( $file->getHistory() as $oldFile ) {
+ /** @var OldLocalFile $oldFile */
+ '@phan-var OldLocalFile $oldFile';
$sha1 = $oldFile->getRepo()->getFileSha1( $oldFile->getPath() );
if ( strval( $sha1 ) !== '' ) { // file on disk and hashed properly
if ( $isRegen && $oldFile->getSha1() !== $sha1 ) {
if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) {
return;
}
+ /** @var WikitextContent $content */
+ '@phan-var WikitextContent $content';
try {
$this->mPreprocessor->preprocessToObj( strval( $content->getText() ), 0 );
$passed = 'passed';
} catch ( Exception $e ) {
$testReport = self::$currentTest->getReport();
- $exceptionReport = $e->getText();
+ $exceptionReport = $e instanceof MWException ? $e->getText() : (string)$e;
$hash = md5( $testReport );
file_put_contents( "results/ppft-$hash.in", serialize( self::$currentTest ) );
file_put_contents( "results/ppft-$hash.fail",
}
/**
- * @return FileRepo
+ * @return LocalRepo
*/
function getRepo() {
if ( !isset( $this->repo ) ) {
$filename = $altname;
$this->output( "Estimating transcoding... $altname\n" );
} else {
- # @todo FIXME: create renameFile()
+ // @fixme create renameFile()
+ // @phan-suppress-next-line PhanUndeclaredMethod See comment above...
$filename = $this->renameFile( $filename );
}
}
[],
[ 'content' => [ 'INNER JOIN', [ 'content_id = slot_content_id' ] ] ]
);
+ /** @var \MediaWiki\Storage\SqlBlobStore $blobStore */
$blobStore = MediaWikiServices::getInstance()->getBlobStore();
+ '@phan-var \MediaWiki\Storage\SqlBlobStore $blobStore';
foreach ( $res as $row ) {
$textId = $blobStore->getTextIdFromAddress( $row->content_address );
if ( $textId ) {
require_once __DIR__ . '/Maintenance.php';
-use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\DatabaseSqlite;
/**
$dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
$this->output( "Going to run database updates for $dbDomain\n" );
if ( $db->getType() === 'sqlite' ) {
- /** @var IMaintainableDatabase|DatabaseSqlite $db */
+ /** @var DatabaseSqlite $db */
+ '@phan-var DatabaseSqlite $db';
$this->output( "Using SQLite file: '{$db->getDbFilePath()}'\n" );
}
$this->output( "Depending on the size of your database this may take a while!\n" );
while ( $json['manifest_version'] !== ExtensionRegistry::MANIFEST_VERSION ) {
$json['manifest_version'] += 1;
$func = "updateTo{$json['manifest_version']}";
+ // @phan-suppress-next-line PhanUndeclaredMethod
$this->$func( $json );
}
*/
class UserDupes {
/**
- * @var IMaintainableDatabase
+ * @var Database
*/
private $db;
private $reassigned;
$user = User::newFromId( $row->user_id );
/** @var ParameterizedPassword $password */
$password = $passwordFactory->newFromCiphertext( $row->user_password );
+ '@phan-var ParameterizedPassword $password';
/** @var LayeredParameterizedPassword $layeredPassword */
$layeredPassword = $passwordFactory->newFromType( $layeredType );
+ '@phan-var LayeredParameterizedPassword $layeredPassword';
$layeredPassword->partialCrypt( $password );
$updateUsers[] = $user;
} );
};
+ // Skeleton user object, extended by the 'mediawiki.user' module.
+ /**
+ * @class mw.user
+ * @singleton
+ */
+ mw.user = {
+ /**
+ * @property {mw.Map}
+ */
+ options: new mw.Map(),
+ /**
+ * @property {mw.Map}
+ */
+ tokens: new mw.Map()
+ };
+
// Alias $j to jQuery for backwards compatibility
// @deprecated since 1.23 Use $ or jQuery instead
mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
* @return {boolean}
*/
ItemModel.prototype.isHighlightSupported = function () {
- return !!this.getCssClass();
+ return !!this.getCssClass() && !OO.ui.isMobile();
};
/**
isEmpty = $changesListContent === 'NO_RESULTS',
// For enhanced mode, we have to load these modules, which are
// not loaded for the 'regular' mode in the backend
- loaderPromise = mw.user.options.get( 'usenewrc' ) ?
+ loaderPromise = mw.user.options.get( 'usenewrc' ) && !OO.ui.isMobile() ?
mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
$.Deferred().resolve(),
widget = this;
controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
) {
var layout,
+ $widgetRow,
classes = [],
$label = $( '<div>' )
.addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
// defaults on 'click' as well.
layout.$label.on( 'click', false );
- this.$element
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
- .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+ $widgetRow = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
.append(
$( '<div>' )
- .addClass( 'mw-rcfilters-ui-table' )
+ .addClass( 'mw-rcfilters-ui-row' )
.append(
$( '<div>' )
- .addClass( 'mw-rcfilters-ui-row' )
- .append(
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
- .append( layout.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
- .append( this.excludeLabel.$element ),
- $( '<div>' )
- .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
- .append( this.highlightButton.$element )
- )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
+ .append( layout.$element )
)
);
+ if ( !OO.ui.isMobile() ) {
+ $widgetRow.find( '.mw-rcfilters-ui-row' ).append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
+ .append( this.excludeLabel.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
+ .append( this.highlightButton.$element )
+ );
+ }
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
+ .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+ .append( $widgetRow );
+
if ( this.itemModel.getIdentifiers() ) {
this.itemModel.getIdentifiers().forEach( function ( ident ) {
classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
hash ^= str.charCodeAt( i );
}
- hash = ( hash >>> 0 ).toString( 36 );
- while ( hash.length < 7 ) {
+ hash = ( hash >>> 0 ).toString( 36 ).slice( 0, 5 );
+ while ( hash.length < 5 ) {
hash = '0' + hash;
}
/* eslint-enable no-bitwise */
// In addition to currReqBase, doRequest() will also add 'modules' and 'version'.
// > '&modules='.length === 9
- // > '&version=1234567'.length === 16
- // > 9 + 16 = 25
- currReqBaseLength = makeQueryString( currReqBase ).length + 25;
+ // > '&version=12345'.length === 14
+ // > 9 + 14 = 23
+ currReqBaseLength = makeQueryString( currReqBase ).length + 23;
// We may need to split up the request to honor the query string length limit,
// so build it piece by piece.
}() )
}
};
- }() ),
-
- // Skeleton user object, extended by the 'mediawiki.user' module.
- /**
- * @class mw.user
- * @singleton
- */
- user: {
- /**
- * @property {mw.Map}
- */
- options: new Map(),
- /**
- * @property {mw.Map}
- */
- tokens: new Map()
- }
-
+ }() )
};
// Attach to window and globally alias
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\Database;
use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
* @since 1.18
$this->fail( $message );
}
+ // If anything faked the time, reset it
+ ConvertibleTimestamp::setFakeTime( false );
+
parent::tearDown();
}
// Version hash for a blank file module.
// Result of ResourceLoader::makeHash(), ResourceLoaderTestModule
// and ResourceLoaderFileModule::getDefinitionSummary().
- const BLANK_VERSION = '09p30q0';
+ const BLANK_VERSION = '9p30q';
+ // Result of ResoureLoader::makeVersionQuery() for a blank file module.
+ // In other words, result of ResourceLoader::makeHash( BLANK_VERSION );
+ const BLANK_COMBI = 'rbml8';
/**
* @param array|string $options Language code or options array
[
[ 'test.quux', ResourceLoaderModule::TYPE_COMBINED ],
"<script nonce=\"secret\">(RLQ=window.RLQ||[]).push(function(){"
- . "mw.loader.implement(\"test.quux@1ev0ijv\",function($,jQuery,require,module){"
+ . "mw.loader.implement(\"test.quux@1ev0i\",function($,jQuery,require,module){"
. "mw.test.baz({token:123});},{\"css\":[\".mw-icon{transition:none}"
. "\"]});});</script>"
],
$op = TestingAccessWrapper::newFromObject( $op );
$op->rlExemptStyleModules = $exemptStyleModules;
+ $expect = strtr( $expect, [
+ '{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
+ ] );
$this->assertEquals(
$expect,
strval( $op->buildExemptModules() )
'exemptStyleModules' => [ 'site' => [ 'site.styles' ], 'user' => [ 'user.styles' ] ],
'<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles"/>' . "\n" .
- '<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=1ai9g6t"/>',
+ '<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=15pue"/>',
],
'custom modules' => [
'exemptStyleModules' => [
'<meta name="ResourceLoaderDynamicStyles" content=""/>' . "\n" .
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.site.a%2Cb&only=styles"/>' . "\n" .
'<link rel="stylesheet" href="/w/load.php?lang=en&modules=site.styles&only=styles"/>' . "\n" .
- '<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.user&only=styles&version=0a56zyi"/>' . "\n" .
- '<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=1ai9g6t"/>',
+ '<link rel="stylesheet" href="/w/load.php?lang=en&modules=example.user&only=styles&version={blankCombi}"/>' . "\n" .
+ '<link rel="stylesheet" href="/w/load.php?lang=en&modules=user.styles&only=styles&version=15pue"/>',
],
];
// phpcs:enable
* @covers FileBackendStoreShardDirIterator
* @covers FileBackendStoreShardFileIterator
* @covers FileBackendStoreShardListIterator
- * @covers FileJournal
* @covers FileOp
* @covers FileOpBatch
* @covers HTTPFileStreamer
* @covers MemoryFileBackend
* @covers MoveFileOp
* @covers MySqlLockManager
- * @covers NullFileJournal
* @covers NullFileOp
* @covers StoreFileOp
* @covers TempFSFile
--- /dev/null
+<?php
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
+/**
+ * @coversDefaultClass DBFileJournal
+ * @covers ::__construct
+ * @covers ::getMasterDB
+ * @group Database
+ */
+class DBFileJournalIntegrationTest extends MediaWikiIntegrationTestCase {
+ public function addDBDataOnce() {
+ global $IP;
+ $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
+ if ( $db->getType() !== 'mysql' ) {
+ return;
+ }
+ if ( !$db->tableExists( 'filejournal' ) ) {
+ $db->sourceFile( "$IP/maintenance/archives/patch-filejournal.sql" );
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
+ if ( $db->getType() !== 'mysql' ) {
+ $this->markTestSkipped( 'No filejournal schema available for this database type' );
+ }
+
+ $this->tablesUsed[] = 'filejournal';
+ }
+
+ private function getJournal( $options = [] ) {
+ return FileJournal::factory(
+ $options + [ 'class' => DBFileJournal::class, 'domain' => wfWikiID() ],
+ 'local-backend' );
+ }
+
+ /**
+ * @covers ::doLogChangeBatch
+ */
+ public function testDoLogChangeBatch_exceptionDbConnect() {
+ $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
+
+ $this->assertEquals(
+ StatusValue::newFatal( 'filejournal-fail-dbconnect', 'local-backend' ),
+ $journal->logChangeBatch( [ [] ], 'batch' ) );
+ }
+
+ /**
+ * @covers ::doLogChangeBatch
+ */
+ public function testDoLogChangeBatch_exceptionDbQuery() {
+ MediaWikiServices::getInstance()->getConfiguredReadOnlyMode()->setReason( 'testing' );
+
+ $journal = $this->getJournal();
+
+ $this->assertEquals(
+ StatusValue::newFatal( 'filejournal-fail-dbquery', 'local-backend' ),
+ $journal->logChangeBatch(
+ [ [ 'op' => null, 'path' => '', 'newSha1' => false ] ], 'batch' ) );
+ }
+
+ /**
+ * @covers ::doLogChangeBatch
+ * @covers ::doGetCurrentPosition
+ */
+ public function testDoGetCurrentPosition() {
+ $journal = $this->getJournal();
+
+ $this->assertNull( $journal->getCurrentPosition() );
+
+ $journal->logChangeBatch(
+ [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
+
+ $this->assertSame( '1', $journal->getCurrentPosition() );
+
+ $journal->logChangeBatch(
+ [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
+
+ $this->assertSame( '2', $journal->getCurrentPosition() );
+ }
+
+ /**
+ * @covers ::doLogChangeBatch
+ * @covers ::doGetPositionAtTime
+ */
+ public function testDoGetPositionAtTime() {
+ $journal = $this->getJournal();
+
+ $now = time();
+
+ $this->assertFalse( $journal->getPositionAtTime( $now ) );
+
+ ConvertibleTimestamp::setFakeTime( $now - 86400 );
+
+ $journal->logChangeBatch(
+ [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
+
+ ConvertibleTimestamp::setFakeTime( $now - 3600 );
+
+ $journal->logChangeBatch(
+ [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
+
+ $this->assertFalse( $journal->getPositionAtTime( $now - 86401 ) );
+ $this->assertSame( '1', $journal->getPositionAtTime( $now - 86400 ) );
+ $this->assertSame( '1', $journal->getPositionAtTime( $now - 3601 ) );
+ $this->assertSame( '2', $journal->getPositionAtTime( $now - 3600 ) );
+ }
+
+ /**
+ * @param int $expectedStart First index expected to be returned (0-based)
+ * @param int|null $expectedCount Number of entries expected to be returned (null for all)
+ * @param string|null|false $expectedNext Expected value of $next, or false not to pass
+ * @param array $args If any third argument is present, $next will also be tested
+ * @dataProvider provideDoGetChangeEntries
+ * @covers ::doLogChangeBatch
+ * @covers ::doGetChangeEntries
+ */
+ public function testDoGetChangeEntries(
+ $expectedStart, $expectedCount, $expectedNext, array $args
+ ) {
+ $journal = $this->getJournal();
+
+ $i = 0;
+ $makeExpectedEntry = function ( $op, $path, $newSha1, $batch, $time ) use ( &$i ) {
+ $i++;
+ return [
+ 'id' => (string)$i,
+ 'batch_uuid' => $batch,
+ 'backend' => 'local-backend',
+ 'path' => $path,
+ 'op' => $op ?? '',
+ 'new_sha1' => $newSha1 !== false ? $newSha1 : '0',
+ 'timestamp' => ConvertibleTimestamp::convert( TS_MW, $time ),
+ ];
+ };
+
+ $expectedEntries = [];
+
+ $now = time();
+
+ ConvertibleTimestamp::setFakeTime( $now - 3600 );
+ $changes = [
+ [ 'op' => 'create', 'path' => '/path1',
+ 'newSha1' => base_convert( sha1( 'a' ), 16, 36 ) ],
+ [ 'op' => 'delete', 'path' => '/path2', 'newSha1' => false ],
+ [ 'op' => 'null', 'path' => '', 'newSha1' => false ],
+ ];
+ $this->assertEquals( StatusValue::newGood(),
+ $journal->logChangeBatch( $changes, 'batch1' ) );
+ foreach ( $changes as $change ) {
+ $expectedEntries[] = $makeExpectedEntry(
+ ...array_merge( array_values( $change ), [ 'batch1', $now - 3600 ] ) );
+ }
+
+ ConvertibleTimestamp::setFakeTime( $now - 60 );
+ $change = [ 'op' => 'update', 'path' => '/path1',
+ 'newSha1' => base_convert( sha1( 'b' ), 16, 36 ) ];
+ $this->assertEquals(
+ StatusValue::newGood(), $journal->logChangeBatch( [ $change ], 'batch2' ) );
+ $expectedEntries[] = $makeExpectedEntry(
+ ...array_merge( array_values( $change ), [ 'batch2', $now - 60 ] ) );
+
+ if ( $expectedNext === false ) {
+ $this->assertSame(
+ array_slice( $expectedEntries, $expectedStart, $expectedCount ),
+ $journal->getChangeEntries( ...$args )
+ );
+ } else {
+ $next = false;
+ $this->assertSame(
+ array_slice( $expectedEntries, $expectedStart, $expectedCount ),
+ $journal->getChangeEntries( $args[0], $args[1], $next )
+ );
+ $this->assertSame( $expectedNext, $next );
+ }
+ }
+
+ public static function provideDoGetChangeEntries() {
+ return [
+ 'No args' => [ 0, 4, false, [] ],
+ 'null' => [ 0, 4, false, [ null ] ],
+ '1' => [ 0, 4, false, [ 1 ] ],
+ '2' => [ 1, 3, false, [ 2 ] ],
+ '4' => [ 3, 1, false, [ 4 ] ],
+ '5' => [ 0, 0, false, [ 5 ] ],
+ 'null, 0' => [ 0, 4, null, [ null, 0 ] ],
+ '1, 0' => [ 0, 4, null, [ 1, 0 ] ],
+ '2, 0' => [ 1, 3, null, [ 2, 0 ] ],
+ '4, 0' => [ 3, 1, null, [ 4, 0 ] ],
+ '5, 0' => [ 0, 0, null, [ 5, 0 ] ],
+ '1, 1' => [ 0, 1, '2', [ 1, 1 ] ],
+ '1, 2' => [ 0, 2, '3', [ 1, 2 ] ],
+ '1, 4' => [ 0, 4, null, [ 1, 4 ] ],
+ '1, 5' => [ 0, 4, null, [ 1, 5 ] ],
+ '2, 2' => [ 1, 2, '4', [ 2, 2 ] ],
+ '1, 2 with no $next' => [ 0, 2, false, [ 1, 2 ] ],
+ ];
+ }
+
+ /**
+ * @covers ::doPurgeOldLogs
+ */
+ public function testDoPurgeOldLogs_noop() {
+ // If we tried to access the database, it would throw, because the domain doesn't exist
+ $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
+ $this->assertEquals( StatusValue::newGood(), $journal->purgeOldLogs() );
+ }
+
+ /**
+ * @covers ::doPurgeOldLogs
+ * @covers ::doLogChangeBatch
+ * @covers ::doGetChangeEntries
+ */
+ public function testDoPurgeOldLogs() {
+ $journal = $this->getJournal( [ 'ttlDays' => 1 ] );
+ $now = time();
+
+ // One day and one second ago
+ ConvertibleTimestamp::setFakeTime( $now - 86401 );
+ $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
+ [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch1' ) );
+
+ // One day ago exactly, won't get purged
+ ConvertibleTimestamp::setFakeTime( $now - 86400 );
+ $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
+ [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch2' ) );
+
+ ConvertibleTimestamp::setFakeTime( $now );
+ $this->assertCount( 2, $journal->getChangeEntries() );
+ $journal->purgeOldLogs();
+ $this->assertCount( 1, $journal->getChangeEntries() );
+ }
+}
[ '.e.x', 'e' ],
[ '..f.x', 'f' ],
[ 'g..x', 'g' ],
+ [ '01234567890123456789012345678901.x', '1234567890123456789012345678901' ],
];
}
// phpcs:disable Generic.Files.LineLength
$expected = '<script>'
- . 'document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");'
+ . 'document.documentElement.className="client-js";'
. 'RLCONF={"key":"value"};'
. 'RLSTATE={"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready"};'
. 'RLPAGEMODULES=["test"];'
);
// phpcs:disable Generic.Files.LineLength
- $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+ $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1&target=example"></script>';
// phpcs:enable
);
// phpcs:disable Generic.Files.LineLength
- $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+ $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1&safemode=1"></script>';
// phpcs:enable
);
// phpcs:disable Generic.Files.LineLength
- $expected = '<script>document.documentElement.className=document.documentElement.className.replace(/(^|\s)client-nojs(\s|$)/,"$1client-js$2");</script>' . "\n"
+ $expected = '<script>document.documentElement.className="client-js";</script>' . "\n"
. '<script async="" src="/w/load.php?lang=nl&modules=startup&only=scripts&raw=1"></script>';
// phpcs:enable
'modules' => [ 'test.scripts.user' ],
'only' => ResourceLoaderModule::TYPE_SCRIPTS,
'extra' => [],
- 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version=0a56zyi");});</script>',
+ 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.scripts.user\u0026only=scripts\u0026user=Example\u0026version={blankCombi}");});</script>',
],
[
'context' => [],
'modules' => [ 'test.user' ],
'only' => ResourceLoaderModule::TYPE_COMBINED,
'extra' => [],
- 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version=0a56zyi");});</script>',
+ 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test.user\u0026user=Example\u0026version={blankCombi}");});</script>',
],
[
'context' => [ 'debug' => 'true' ],
'modules' => [ 'test.shouldembed' ],
'only' => ResourceLoaderModule::TYPE_COMBINED,
'extra' => [],
- 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+ 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@{blankVer}",null,{"css":[]});});</script>',
],
[
'context' => [],
'modules' => [ 'test', 'test.shouldembed' ],
'only' => ResourceLoaderModule::TYPE_COMBINED,
'extra' => [],
- 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
+ 'output' => '<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?lang=nl\u0026modules=test");mw.loader.implement("test.shouldembed@{blankVer}",null,{"css":[]});});</script>',
],
[
'context' => [],
private static function expandVariables( $text ) {
return strtr( $text, [
+ '{blankCombi}' => ResourceLoaderTestCase::BLANK_COMBI,
'{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
] );
}
'factory' => function () {
$mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
->setMethods( [ 'getVersionHash' ] )->getMock();
- $mock->method( 'getVersionHash' )->willReturn( '1234567' );
+ $mock->method( 'getVersionHash' )->willReturn( '12345' );
return $mock;
}
]
mw.loader.register([
[
"test.version",
- "1234567"
+ "12345"
]
]);',
] ],
mw.loader.register([
[
"test.version",
- "016es8l"
+ "16es8"
]
]);',
] ],
);
$this->assertEquals(
- ResourceLoader::makeHash( self::BLANK_VERSION ),
+ self::BLANK_COMBI,
$rl->getCombinedVersion( $context, [ 'foo' ] ),
'compute foo'
);
--- /dev/null
+<?php
+
+require_once __DIR__ . '/TestFileJournal.php';
+
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
+/**
+ * @coversDefaultClass FileJournal
+ */
+class FileJournalTest extends MediaWikiUnitTestCase {
+ private function newObj( $options = [], $backend = '' ) {
+ return FileJournal::factory(
+ $options + [ 'class' => TestFileJournal::class ],
+ $backend
+ );
+ }
+
+ /**
+ * @covers ::factory
+ */
+ public function testConstructor_backend() {
+ $this->assertSame( 'some_backend', $this->newObj( [], 'some_backend' )->getBackend() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::factory
+ */
+ public function testConstructor_ttlDays() {
+ $this->assertSame( 42, $this->newObj( [ 'ttlDays' => 42 ] )->getTtlDays() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::factory
+ */
+ public function testConstructor_noTtlDays() {
+ $this->assertSame( false, $this->newObj()->getTtlDays() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::factory
+ */
+ public function testConstructor_nullTtlDays() {
+ $this->assertSame( false, $this->newObj( [ 'ttlDays' => null ] )->getTtlDays() );
+ }
+
+ /**
+ * @covers ::factory
+ */
+ public function testFactory_invalidClass() {
+ $this->setExpectedException( UnexpectedValueException::class,
+ 'Expected instance of FileJournal, got stdClass' );
+
+ FileJournal::factory( [ 'class' => 'stdclass' ], '' );
+ }
+
+ /**
+ * @covers ::getTimestampedUUID
+ */
+ public function testGetTimestampedUUID() {
+ $obj = FileJournal::factory( [ 'class' => 'NullFileJournal' ], '' );
+ $uuids = [];
+ for ( $i = 0; $i < 10; $i++ ) {
+ $time1 = time();
+ $uuid = $obj->getTimestampedUUID();
+ $time2 = time();
+ $this->assertRegexp( '/^[0-9a-z]{31}$/', $uuid );
+ $this->assertArrayNotHasKey( $uuid, $uuids );
+ $uuids[$uuid] = true;
+
+ // Now test that the timestamp portion is as expected.
+ $time = ConvertibleTimestamp::convert( TS_UNIX, Wikimedia\base_convert(
+ substr( $uuid, 0, 9 ), 36, 10 ) );
+
+ $this->assertGreaterThanOrEqual( $time1, $time );
+ $this->assertLessThanOrEqual( $time2, $time );
+ }
+ }
+
+ /**
+ * @covers ::logChangeBatch
+ */
+ public function testLogChangeBatch() {
+ $this->assertEquals(
+ StatusValue::newGood( 'Logged' ), $this->newObj()->logChangeBatch( [ 1 ], '' ) );
+ }
+
+ /**
+ * @covers ::logChangeBatch
+ */
+ public function testLogChangeBatch_empty() {
+ $this->assertEquals( StatusValue::newGood(), $this->newObj()->logChangeBatch( [], '' ) );
+ }
+
+ /**
+ * @covers ::getCurrentPosition
+ */
+ public function testGetCurrentPosition() {
+ $this->assertEquals( 613, $this->newObj()->getCurrentPosition() );
+ }
+
+ /**
+ * @covers ::getPositionAtTime
+ */
+ public function testGetPositionAtTime() {
+ $this->assertEquals( 248, $this->newObj()->getPositionAtTime( 0 ) );
+ }
+
+ /**
+ * @dataProvider provideGetChangeEntries
+ * @covers ::getChangeEntries
+ * @param int|null $start
+ * @param int $limit
+ * @param string|null $expectedNext
+ * @param string[] $expectedReturn Expected id's of returned values
+ */
+ public function testGetChangeEntries( $start, $limit, $expectedNext, array $expectedReturn ) {
+ $expectedReturn = array_map(
+ function ( $val ) {
+ return [ 'id' => $val ];
+ }, $expectedReturn
+ );
+ $next = "Different from $expectedNext";
+ $ret = $this->newObj()->getChangeEntries( $start, $limit, $next );
+ $this->assertSame( $expectedNext, $next );
+ $this->assertSame( $expectedReturn, $ret );
+ }
+
+ public static function provideGetChangeEntries() {
+ return [
+ [ null, 0, null, [ 1, 2, 3 ] ],
+ [ null, 1, 2, [ 1 ] ],
+ [ null, 2, 3, [ 1, 2 ] ],
+ [ null, 3, null, [ 1, 2, 3 ] ],
+ [ 1, 0, null, [ 1, 2, 3 ] ],
+ [ 1, 2, 3, [ 1, 2 ] ],
+ [ 1, 1, 2, [ 1 ] ],
+ [ 2, 2, null, [ 2, 3 ] ],
+ ];
+ }
+
+ /**
+ * @covers ::purgeOldLogs
+ */
+ public function testPurgeOldLogs() {
+ $obj = $this->newObj();
+ $this->assertFalse( $obj->getPurged() );
+ $obj->purgeOldLogs();
+ $this->assertTrue( $obj->getPurged() );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @coversDefaultClass NullFileJournal
+ */
+class NullFileJournalTest extends MediaWikiUnitTestCase {
+ public function newObj() : NullFileJournal {
+ return FileJournal::factory( [ 'class' => NullFileJournal::class ], '' );
+ }
+
+ /**
+ * @covers ::doLogChangeBatch
+ */
+ public function testLogChangeBatch() {
+ $this->assertEquals( StatusValue::newGood(), $this->newObj()->logChangeBatch( [ 1 ], '' ) );
+ }
+
+ /**
+ * @covers ::doGetCurrentPosition
+ */
+ public function testGetCurrentPosition() {
+ $this->assertFalse( $this->newObj()->getCurrentPosition() );
+ }
+
+ /**
+ * @covers ::doGetPositionAtTime
+ */
+ public function testGetPositionAtTime() {
+ $this->assertFalse( $this->newObj()->getPositionAtTime( 2 ) );
+ }
+
+ /**
+ * @covers ::doGetChangeEntries
+ */
+ public function testGetChangeEntries() {
+ $next = 1;
+ $entries = $this->newObj()->getChangeEntries( null, 0, $next );
+ $this->assertSame( [], $entries );
+ $this->assertNull( $next );
+ }
+
+ /**
+ * @covers ::doPurgeOldLogs
+ */
+ public function testPurgeOldLogs() {
+ $this->assertEquals( StatusValue::newGood(), $this->newObj()->purgeOldLogs() );
+ }
+}
--- /dev/null
+<?php
+
+class TestFileJournal extends NullFileJournal {
+ /** @var bool */
+ private $purged = false;
+
+ public function getTtlDays() {
+ return $this->ttlDays;
+ }
+
+ public function getBackend() {
+ return $this->backend;
+ }
+
+ protected function doLogChangeBatch( array $entries, $batchId ) {
+ return StatusValue::newGood( 'Logged' );
+ }
+
+ protected function doGetCurrentPosition() {
+ return 613;
+ }
+
+ protected function doGetPositionAtTime( $time ) {
+ return 248;
+ }
+
+ protected function doGetChangeEntries( $start, $limit ) {
+ return array_slice( [
+ [ 'id' => 1 ],
+ [ 'id' => 2 ],
+ [ 'id' => 3 ],
+ ], $start === null ? 0 : $start - 1, $limit ? $limit : null );
+ }
+
+ protected function doPurgeOldLogs() {
+ $this->purged = true;
+ }
+
+ public function getPurged() {
+ return $this->purged;
+ }
+}
require( 'testUrlIncDump' ).query,
{
modules: 'testUrlIncDump',
- // Expected: Wrapped hash just for this one module
- // $hash = hash( 'fnv132', 'dump');
- // base_convert( $hash, 16, 36 ); // "13e9zzn"
- // Previously: Wrapped hash for both modules, despite being in separate requests
- // $hash = hash( 'fnv132', 'urldump' );
- // base_convert( $hash, 16, 36 ); // "18kz9ca"
- version: '13e9zzn'
+ // Expected: Combine hashes only for the module in the specific HTTP request
+ // hash fnv132 => "13e9zzn"
+ // Wrong: Combine hashes for all requested modules, before request-splitting
+ // hash fnv132 => "18kz9ca"
+ version: '13e9z'
},
'Query parameters'
);
require( 'testUrlOrderDump' ).query,
{
modules: 'testUrlOrder,testUrlOrderDump|testUrlOrder.a,b',
- // Expected: Combined in order after string packing
- // $hash = hash( 'fnv132', 'urldump12' );
- // base_convert( $hash, 16, 36 ); // "1knqzan"
- // Previously: Combined in order of before string packing
- // $hash = hash( 'fnv132', 'url12dump' );
- // base_convert( $hash, 16, 36 ); // "11eo3in"
- version: '1knqzan'
+ // Expected: Combined by sorting names after string packing
+ // hash fnv132 = "1knqzan"
+ // Wrong: Combined by sorting names before string packing
+ // hash fnv132 => "11eo3in"
+ version: '1knqz'
},
'Query parameters'
);