* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
=== Other changes in 1.31 ===
+* Introducing multi-content-revision capability into the storage layer. For details,
+ see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
+* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
+ RevisionRecord and its subclasses.
* MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
* The global function wfBCP47 was renamed to LanguageCode::bcp47.
* The global function wfBCP47 is now deprecated.
* The Block class will no longer accept usable-but-missing usernames for
'byText' or ->setBlocker(). Callers should either ensure the blocker exists
locally or use a new interwiki-format username like "iw>Example".
+* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
+ RevisionInsertComplete is still called, but the second and third parameter will always be null.
+ Hard deprecation is scheduled for 1.32.
* The following methods that get and set ParserOutput state are deprecated.
Callers should use the new stateless $options parameter to
ParserOutput::getText() instead.
added to any module.
&$ResourceLoader: object
-'RevisionInsertComplete': Called after a revision is inserted into the database.
-&$revision: the Revision
-$data: the data stored in old_text. The meaning depends on $flags: if external
- is set, it's the URL of the revision text in external storage; otherwise,
- it's the revision text itself. In either case, if gzip is set, the revision
- text is gzipped.
-$flags: a comma-delimited list of strings representing the options used. May
- include: utf8 (this will always be set for new revisions); gzip; external.
+'RevisionRecordInserted': Called after a revision is inserted into the database.
+$revisionRecord: the RevisionRecord that has just been inserted.
+
+'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
+Called after a revision is inserted into the database.
+$revision: the Revision
+$data: DEPRECATED! Always null!
+$flags: DEPRECATED! Always null!
'SearchableNamespaces': An option to modify which namespaces are searchable.
&$arr: Array of namespaces ($nsId => $name) which will be used.
* @file
*/
-use Wikimedia\Rdbms\Database;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\RevisionStoreRecord;
+use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentityValue;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\FakeResultWrapper;
/**
- * @todo document
+ * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
*/
class Revision implements IDBAccessObject {
- /** @var int|null */
- protected $mId;
- /** @var int|null */
- protected $mPage;
- /** @var string */
- protected $mUserText;
- /** @var string */
- protected $mOrigUserText;
- /** @var int */
- protected $mUser;
- /** @var bool */
- protected $mMinorEdit;
- /** @var string */
- protected $mTimestamp;
- /** @var int */
- protected $mDeleted;
- /** @var int */
- protected $mSize;
- /** @var string */
- protected $mSha1;
- /** @var int */
- protected $mParentId;
- /** @var string */
- protected $mComment;
- /** @var string */
- protected $mText;
- /** @var int */
- protected $mTextId;
- /** @var int */
- protected $mUnpatrolled;
-
- /** @var stdClass|null */
- protected $mTextRow;
-
- /** @var null|Title */
- protected $mTitle;
- /** @var bool */
- protected $mCurrent;
- /** @var string */
- protected $mContentModel;
- /** @var string */
- protected $mContentFormat;
-
- /** @var Content|null|bool */
- protected $mContent;
- /** @var null|ContentHandler */
- protected $mContentHandler;
-
- /** @var int */
- protected $mQueryFlags = 0;
- /** @var bool Used for cached values to reload user text and rev_deleted */
- protected $mRefreshMutableFields = false;
- /** @var string Wiki ID; false means the current wiki */
- protected $mWiki = false;
+
+ /** @var RevisionRecord */
+ protected $mRecord;
// Revision deletion constants
- const DELETED_TEXT = 1;
- const DELETED_COMMENT = 2;
- const DELETED_USER = 4;
- const DELETED_RESTRICTED = 8;
- const SUPPRESSED_USER = 12; // convenience
- const SUPPRESSED_ALL = 15; // convenience
+ const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
+ const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
+ const DELETED_USER = RevisionRecord::DELETED_USER;
+ const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
+ const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
+ const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
// Audience options for accessors
- const FOR_PUBLIC = 1;
- const FOR_THIS_USER = 2;
- const RAW = 3;
+ const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
+ const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
+ const RAW = RevisionRecord::RAW;
+
+ const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
- const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
+ /**
+ * @return RevisionStore
+ */
+ protected static function getRevisionStore() {
+ return MediaWikiServices::getInstance()->getRevisionStore();
+ }
+
+ /**
+ * @return SqlBlobStore
+ */
+ protected static function getBlobStore() {
+ $store = MediaWikiServices::getInstance()->getBlobStore();
+
+ if ( !$store instanceof SqlBlobStore ) {
+ throw new RuntimeException(
+ 'The backwards compatibility code in Revision currently requires the BlobStore '
+ . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
+ );
+ }
+
+ return $store;
+ }
/**
* Load a page revision from a given revision ID number.
* @return Revision|null
*/
public static function newFromId( $id, $flags = 0 ) {
- return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+ $rec = self::getRevisionStore()->getRevisionById( $id, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* @return Revision|null
*/
public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
- $conds = [
- 'page_namespace' => $linkTarget->getNamespace(),
- 'page_title' => $linkTarget->getDBkey()
- ];
- if ( $id ) {
- // Use the specified ID
- $conds['rev_id'] = $id;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id=page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* @return Revision|null
*/
public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
- $conds = [ 'page_id' => $pageId ];
- if ( $revId ) {
- $conds['rev_id'] = $revId;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id = page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* Make a fake revision object from an archive table row. This is queried
* for permissions or even inserted (as in Special:Undelete)
- * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
*
* @param object $row
* @param array $overrides
* @return Revision
*/
public static function newFromArchiveRow( $row, $overrides = [] ) {
- global $wgContentHandlerUseDB;
-
- $attribs = $overrides + [
- 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
- 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
- 'comment' => CommentStore::newKey( 'ar_comment' )
- // Legacy because $row may have come from self::selectArchiveFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
- 'user' => $row->ar_user,
- 'user_text' => $row->ar_user_text,
- 'timestamp' => $row->ar_timestamp,
- 'minor_edit' => $row->ar_minor_edit,
- 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
- 'deleted' => $row->ar_deleted,
- 'len' => $row->ar_len,
- 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
- 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
- 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
- ];
-
- if ( !$wgContentHandlerUseDB ) {
- unset( $attribs['content_model'] );
- unset( $attribs['content_format'] );
- }
-
- if ( !isset( $attribs['title'] )
- && isset( $row->ar_namespace )
- && isset( $row->ar_title )
- ) {
- $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
- }
-
- if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
- // Pre-1.5 ar_text row
- $attribs['text'] = self::getRevisionText( $row, 'ar_' );
- if ( $attribs['text'] === false ) {
- throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
- }
- }
- return new self( $attribs );
+ $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, null, $overrides );
+ return new Revision( $rec );
}
/**
* @since 1.19
*
- * @param object $row
+ * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
+ * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
+ * a MutableRevisionRecord should be constructed directly. RevisionStore::newRevisionFromArray()
+ * can be used as a temporary replacement, but should be avoided.
+ *
+ * @param object|array $row
* @return Revision
*/
public static function newFromRow( $row ) {
- return new self( $row );
+ if ( is_array( $row ) ) {
+ $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row );
+ } else {
+ $rec = self::getRevisionStore()->newRevisionFromRow( $row );
+ }
+
+ return new Revision( $rec );
}
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
+ *
* @param IDatabase $db
* @param int $id
* @return Revision|null
*/
public static function loadFromId( $db, $id ) {
- return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+ wfDeprecated( __METHOD__, '1.31' ); // no known callers
+ $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
+ *
* @param IDatabase $db
* @param int $pageid
* @param int $id
* @return Revision|null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
- if ( $id ) {
- $conds['rev_id'] = intval( $id );
- } else {
- $conds[] = 'rev_id=page_latest';
- }
- return self::loadFromConds( $db, $conds );
+ $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param int $id
* @return Revision|null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
- if ( $id ) {
- $matchId = intval( $id );
- } else {
- $matchId = 'page_latest';
- }
- return self::loadFromConds( $db,
- [
- "rev_id=$matchId",
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
+ $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* WARNING: Timestamps may in some circumstances not be unique,
* so this isn't the best key to use.
*
+ * @deprecated since 1.31, use RevisionStore::loadRevisionFromTimestamp() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param string $timestamp
* @return Revision|null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
- return self::loadFromConds( $db,
- [
- 'rev_timestamp' => $db->timestamp( $timestamp ),
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
- }
-
- /**
- * Given a set of conditions, fetch a revision
- *
- * This method is used then a revision ID is qualified and
- * will incorporate some basic replica DB/master fallback logic
- *
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function newFromConds( $conditions, $flags = 0 ) {
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-
- $rev = self::loadFromConds( $db, $conditions, $flags );
- // Make sure new pending/committed revision are visibile later on
- // within web requests to certain avoid bugs like T93866 and T94407.
- if ( !$rev
- && !( $flags & self::READ_LATEST )
- && wfGetLB()->getServerCount() > 1
- && wfGetLB()->hasOrMadeRecentMasterChanges()
- ) {
- $flags = self::READ_LATEST;
- $db = wfGetDB( DB_MASTER );
- $rev = self::loadFromConds( $db, $conditions, $flags );
- }
-
- if ( $rev ) {
- $rev->mQueryFlags = $flags;
- }
-
- return $rev;
- }
-
- /**
- * Given a set of conditions, fetch a revision from
- * the given database connection.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function loadFromConds( $db, $conditions, $flags = 0 ) {
- $row = self::fetchFromConds( $db, $conditions, $flags );
- if ( $row ) {
- $rev = new Revision( $row );
- $rev->mWiki = $db->getDomainID();
-
- return $rev;
- }
-
- return null;
+ // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp?
+ $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
+ return $rec === null ? null : new Revision( $rec );
}
/**
*
* @param LinkTarget $title
* @return ResultWrapper
- * @deprecated Since 1.28
+ * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
*/
public static function fetchRevision( LinkTarget $title ) {
- $row = self::fetchFromConds(
- wfGetDB( DB_REPLICA ),
- [
- 'rev_id=page_latest',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
-
- return new FakeResultWrapper( $row ? [ $row ] : [] );
- }
-
- /**
- * Given a set of conditions, return a ResultWrapper
- * which will return matching database rows with the
- * fields necessary to build Revision objects.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return stdClass
- */
- private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
- $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
- $options = [];
- if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
- $options[] = 'FOR UPDATE';
- }
- return $db->selectRow(
- $revQuery['tables'],
- $revQuery['fields'],
- $conditions,
- __METHOD__,
- $options,
- $revQuery['joins']
- );
+ wfDeprecated( __METHOD__, '1.31' );
+ return new FakeResultWrapper( [] );
}
/**
* Return the value of a select() JOIN conds array for the user table.
* This will get user table rows for logged-in users.
* @since 1.19
- * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function userJoinCond() {
* Return the value of a select() page conds array for the page table.
* This will assure that the revision(s) are not orphaned from live pages.
* @since 1.19
- * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function pageJoinCond() {
/**
* Return the list of revision fields that should be selected to create
* a new revision.
- * @deprecated since 1.31, use self::getQueryInfo() instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
/**
* Return the list of revision fields that should be selected to create
* a new revision from an archive row.
- * @deprecated since 1.31, use self::getArchiveQueryInfo() instead.
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @return array
*/
public static function selectArchiveFields() {
/**
* Return the list of text fields that should be selected to read the
* revision text
- * @deprecated since 1.31, use self::getQueryInfo( [ 'text' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
* @return array
*/
public static function selectTextFields() {
/**
* Return the list of page fields that should be selected from page table
- * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function selectPageFields() {
/**
* Return the list of user fields that should be selected from user table
- * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function selectUserFields() {
* Return the tables, fields, and join conditions to be selected to create
* a new revision object.
* @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @param array $options Any combination of the following strings
* - 'page': Join with the page table, and select fields to identify the page
* - 'user': Join with the user table, and select the user name
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getQueryInfo( $options = [] ) {
- global $wgContentHandlerUseDB;
-
- $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
- $ret = [
- 'tables' => [ 'revision' ] + $commentQuery['tables'],
- 'fields' => [
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
- 'rev_minor_edit',
- 'rev_deleted',
- 'rev_len',
- 'rev_parent_id',
- 'rev_sha1',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $ret['fields'][] = 'rev_content_format';
- $ret['fields'][] = 'rev_content_model';
- }
-
- if ( in_array( 'page', $options, true ) ) {
- $ret['tables'][] = 'page';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ] );
- $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
- }
-
- if ( in_array( 'user', $options, true ) ) {
- $ret['tables'][] = 'user';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'user_name',
- ] );
- $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
- }
-
- if ( in_array( 'text', $options, true ) ) {
- $ret['tables'][] = 'text';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'old_text',
- 'old_flags'
- ] );
- $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
- }
-
- return $ret;
+ return self::getRevisionStore()->getQueryInfo( $options );
}
/**
* Return the tables, fields, and join conditions to be selected to create
* a new archived revision object.
* @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getArchiveQueryInfo() {
- global $wgContentHandlerUseDB;
-
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
- $ret = [
- 'tables' => [ 'archive' ] + $commentQuery['tables'],
- 'fields' => [
- 'ar_id',
- 'ar_page_id',
- 'ar_rev_id',
- 'ar_text',
- 'ar_text_id',
- 'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
- 'ar_minor_edit',
- 'ar_deleted',
- 'ar_len',
- 'ar_parent_id',
- 'ar_sha1',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $ret['fields'][] = 'ar_content_format';
- $ret['fields'][] = 'ar_content_model';
- }
-
- return $ret;
+ return self::getRevisionStore()->getArchiveQueryInfo();
}
/**
* @return array
*/
public static function getParentLengths( $db, array $revIds ) {
- $revLens = [];
- if ( !$revIds ) {
- return $revLens; // empty
- }
- $res = $db->select( 'revision',
- [ 'rev_id', 'rev_len' ],
- [ 'rev_id' => $revIds ],
- __METHOD__ );
- foreach ( $res as $row ) {
- $revLens[$row->rev_id] = $row->rev_len;
- }
- return $revLens;
+ return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
}
/**
- * @param object|array $row Either a database row or an array
- * @throws MWException
+ * @param object|array|RevisionRecord $row Either a database row or an array
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
* @access private
*/
- public function __construct( $row ) {
- if ( is_object( $row ) ) {
- $this->constructFromDbRowObject( $row );
- } elseif ( is_array( $row ) ) {
- $this->constructFromRowArray( $row );
- } else {
- throw new MWException( 'Revision constructor passed invalid row format.' );
- }
- $this->mUnpatrolled = null;
- }
+ function __construct( $row, $queryFlags = 0, Title $title = null ) {
+ global $wgUser;
- /**
- * @param object $row
- */
- private function constructFromDbRowObject( $row ) {
- $this->mId = intval( $row->rev_id );
- $this->mPage = intval( $row->rev_page );
- $this->mTextId = intval( $row->rev_text_id );
- $this->mComment = CommentStore::newKey( 'rev_comment' )
- // Legacy because $row may have come from self::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
- $this->mUser = intval( $row->rev_user );
- $this->mMinorEdit = intval( $row->rev_minor_edit );
- $this->mTimestamp = $row->rev_timestamp;
- $this->mDeleted = intval( $row->rev_deleted );
-
- if ( !isset( $row->rev_parent_id ) ) {
- $this->mParentId = null;
- } else {
- $this->mParentId = intval( $row->rev_parent_id );
- }
-
- if ( !isset( $row->rev_len ) ) {
- $this->mSize = null;
- } else {
- $this->mSize = intval( $row->rev_len );
- }
-
- if ( !isset( $row->rev_sha1 ) ) {
- $this->mSha1 = null;
- } else {
- $this->mSha1 = $row->rev_sha1;
- }
-
- if ( isset( $row->page_latest ) ) {
- $this->mCurrent = ( $row->rev_id == $row->page_latest );
- $this->mTitle = Title::newFromRow( $row );
- } else {
- $this->mCurrent = false;
- $this->mTitle = null;
- }
-
- if ( !isset( $row->rev_content_model ) ) {
- $this->mContentModel = null; # determine on demand if needed
- } else {
- $this->mContentModel = strval( $row->rev_content_model );
- }
-
- if ( !isset( $row->rev_content_format ) ) {
- $this->mContentFormat = null; # determine on demand if needed
- } else {
- $this->mContentFormat = strval( $row->rev_content_format );
- }
+ if ( $row instanceof RevisionRecord ) {
+ $this->mRecord = $row;
+ } elseif ( is_array( $row ) ) {
+ if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
+ $row['user'] = $wgUser;
+ }
- // Lazy extraction...
- $this->mText = null;
- if ( isset( $row->old_text ) ) {
- $this->mTextRow = $row;
+ $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray(
+ $row,
+ $queryFlags,
+ $title
+ );
+ } elseif ( is_object( $row ) ) {
+ $this->mRecord = self::getRevisionStore()->newRevisionFromRow(
+ $row,
+ $queryFlags,
+ $title
+ );
} else {
- // 'text' table row entry will be lazy-loaded
- $this->mTextRow = null;
- }
-
- // Use user_name for users and rev_user_text for IPs...
- $this->mUserText = null; // lazy load if left null
- if ( $this->mUser == 0 ) {
- $this->mUserText = $row->rev_user_text; // IP user
- } elseif ( isset( $row->user_name ) ) {
- $this->mUserText = $row->user_name; // logged-in user
+ throw new InvalidArgumentException(
+ '$row must be a row object, an associative array, or a RevisionRecord'
+ );
}
- $this->mOrigUserText = $row->rev_user_text;
}
/**
- * @param array $row
- *
- * @throws MWException
+ * @return RevisionRecord
*/
- private function constructFromRowArray( array $row ) {
- // Build a new revision to be saved...
- global $wgUser; // ugh
-
- # if we have a content object, use it to set the model and type
- if ( !empty( $row['content'] ) ) {
- if ( !( $row['content'] instanceof Content ) ) {
- throw new MWException( '`content` field must contain a Content object.' );
- }
-
- // @todo when is that set? test with external store setup! check out insertOn() [dk]
- if ( !empty( $row['text_id'] ) ) {
- throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
- "can't serialize content object" );
- }
-
- $row['content_model'] = $row['content']->getModel();
- # note: mContentFormat is initializes later accordingly
- # note: content is serialized later in this method!
- # also set text to null?
- }
-
- $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
- $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
- $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
- $this->mUserText = isset( $row['user_text'] )
- ? strval( $row['user_text'] ) : $wgUser->getName();
- $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
- $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
- $this->mTimestamp = isset( $row['timestamp'] )
- ? strval( $row['timestamp'] ) : wfTimestampNow();
- $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
- $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
- $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
- $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
- $this->mContentModel = isset( $row['content_model'] )
- ? strval( $row['content_model'] ) : null;
- $this->mContentFormat = isset( $row['content_format'] )
- ? strval( $row['content_format'] ) : null;
-
- // Enforce spacing trimming on supplied text
- $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
- $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
- $this->mTextRow = null;
-
- $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
-
- // if we have a Content object, override mText and mContentModel
- if ( !empty( $row['content'] ) ) {
- $handler = $this->getContentHandler();
- $this->mContent = $row['content'];
-
- $this->mContentModel = $this->mContent->getModel();
- $this->mContentHandler = null;
-
- $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
- } elseif ( $this->mText !== null ) {
- $handler = $this->getContentHandler();
- $this->mContent = $handler->unserializeContent( $this->mText );
- }
-
- // If we have a Title object, make sure it is consistent with mPage.
- if ( $this->mTitle && $this->mTitle->exists() ) {
- if ( $this->mPage === null ) {
- // if the page ID wasn't known, set it now
- $this->mPage = $this->mTitle->getArticleID();
- } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
- // Got different page IDs. This may be legit (e.g. during undeletion),
- // but it seems worth mentioning it in the log.
- wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
- $this->mTitle->getArticleID() . " provided by the Title object." );
- }
- }
-
- $this->mCurrent = false;
-
- // If we still have no length, see it we have the text to figure it out
- if ( !$this->mSize && $this->mContent !== null ) {
- $this->mSize = $this->mContent->getSize();
- }
-
- // Same for sha1
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
- }
-
- // force lazy init
- $this->getContentModel();
- $this->getContentFormat();
+ public function getRevisionRecord() {
+ return $this->mRecord;
}
/**
* @return int|null
*/
public function getId() {
- return $this->mId;
+ return $this->mRecord->getId();
}
/**
* Set the revision ID
*
- * This should only be used for proposed revisions that turn out to be null edits
+ * This should only be used for proposed revisions that turn out to be null edits.
+ *
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
*
* @since 1.19
- * @param int $id
+ * @param int|string $id
+ * @throws MWException
*/
public function setId( $id ) {
- $this->mId = (int)$id;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setId( intval( $id ) );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
*
* This should only be used for proposed revisions that turn out to be null edits
*
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
+ *
* @since 1.28
* @deprecated since 1.31, please reuse old Revision object
* @param int $id User ID
* @param string $name User name
+ * @throws MWException
*/
public function setUserIdAndName( $id, $name ) {
- $this->mUser = (int)$id;
- $this->mUserText = $name;
- $this->mOrigUserText = $name;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $user = new UserIdentityValue( intval( $id ), $name );
+ $this->mRecord->setUser( $user );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
- * Get text row ID
+ * @return SlotRecord
+ */
+ private function getMainSlotRaw() {
+ return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
+ }
+
+ /**
+ * Get the ID of the row of the text table that contains the content of the
+ * revision's main slot, if that content is stored in the text table.
+ *
+ * If the content is stored elsewhere, this returns null.
+ *
+ * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
+ * get that actual address that can be used with BlobStore::getBlob(); or use
+ * RevisionRecord::hasSameContent() to check if two revisions have the same content.
*
* @return int|null
*/
public function getTextId() {
- return $this->mTextId;
+ $slot = $this->getMainSlotRaw();
+ return $slot->hasAddress()
+ ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
+ : null;
}
/**
* Get parent revision ID (the original previous page revision)
*
- * @return int|null
+ * @return int|null The ID of the parent revision. 0 indicates that there is no
+ * parent revision. Null indicates that the parent revision is not known.
*/
public function getParentId() {
- return $this->mParentId;
+ return $this->mRecord->getParentId();
}
/**
* Returns the length of the text in this revision, or null if unknown.
*
- * @return int|null
+ * @return int
*/
public function getSize() {
- return $this->mSize;
+ return $this->mRecord->getSize();
}
/**
- * Returns the base36 sha1 of the text in this revision, or null if unknown.
+ * Returns the base36 sha1 of the content in this revision, or null if unknown.
*
- * @return string|null
+ * @return string
*/
public function getSha1() {
- return $this->mSha1;
+ // XXX: we may want to drop all the hashing logic, it's not worth the overhead.
+ return $this->mRecord->getSha1();
}
/**
- * Returns the title of the page associated with this entry or null.
+ * Returns the title of the page associated with this entry.
+ * Since 1.31, this will never return null.
*
* Will do a query, when title is not set and id is given.
*
- * @return Title|null
+ * @return Title
*/
public function getTitle() {
- if ( $this->mTitle !== null ) {
- return $this->mTitle;
- }
- // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
- if ( $this->mId !== null ) {
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
- $row = $dbr->selectRow(
- [ 'revision', 'page' ],
- [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ],
- [ 'rev_id' => $this->mId ],
- __METHOD__,
- [],
- [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
- );
- if ( $row ) {
- // @TODO: better foreign title handling
- $this->mTitle = Title::newFromRow( $row );
- }
- }
-
- if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
- // Loading by ID is best, though not possible for foreign titles
- if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
- $this->mTitle = Title::newFromID( $this->mPage );
- }
- }
-
- return $this->mTitle;
+ $linkTarget = $this->mRecord->getPageAsLinkTarget();
+ return Title::newFromLinkTarget( $linkTarget );
}
/**
* Set the title of the revision
*
+ * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
+ *
* @param Title $title
*/
public function setTitle( $title ) {
- $this->mTitle = $title;
+ if ( !$title->equals( $this->getTitle() ) ) {
+ throw new InvalidArgumentException(
+ $title->getPrefixedText()
+ . ' is not the same as '
+ . $this->mRecord->getPageAsLinkTarget()->__toString()
+ );
+ }
}
/**
* @return int|null
*/
public function getPage() {
- return $this->mPage;
+ return $this->mRecord->getPageId();
}
/**
* @return int
*/
public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return 0;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return 0;
- } else {
- return $this->mUser;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getId() : 0;
}
/**
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
- $this->loadMutableFields();
+ global $wgUser;
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return '';
- } else {
- if ( $this->mUserText === null ) {
- $this->mUserText = User::whoIs( $this->mUser ); // load on demand
- if ( $this->mUserText === false ) {
- # This shouldn't happen, but it can if the wiki was recovered
- # via importing revs and there is no user table entry yet.
- $this->mUserText = $this->mOrigUserText;
- }
- }
- return $this->mUserText;
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getName() : '';
}
/**
* @return string
*/
function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
- return '';
- } else {
- return $this->mComment;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $comment = $this->mRecord->getComment( $audience, $user );
+ return $comment === null ? null : $comment->text;
}
/**
* @return bool
*/
public function isMinor() {
- return (bool)$this->mMinorEdit;
+ return $this->mRecord->isMinor();
}
/**
* @return int Rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- if ( $this->mUnpatrolled !== null ) {
- return $this->mUnpatrolled;
- }
- $rc = $this->getRecentChange();
- if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
- $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
- } else {
- $this->mUnpatrolled = 0;
- }
- return $this->mUnpatrolled;
+ return self::getRevisionStore()->isUnpatrolled( $this->mRecord );
}
/**
* @return RecentChange|null
*/
public function getRecentChange( $flags = 0 ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
-
- return RecentChange::newFromConds(
- [
- 'rc_user_text' => $this->getUserText( self::RAW ),
- 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- 'rc_this_oldid' => $this->getId()
- ],
- __METHOD__,
- $dbType
- );
+ return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
}
/**
* @return bool
*/
public function isDeleted( $field ) {
- if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
- // Current revisions of pages cannot have the content hidden. Skipping this
- // check is very useful for Parser as it fetches templates using newKnownCurrent().
- // Calling getVisibility() in that case triggers a verification database query.
- return false; // no need to check
- }
-
- return ( $this->getVisibility() & $field ) == $field;
+ return $this->mRecord->isDeleted( $field );
}
/**
* @return int
*/
public function getVisibility() {
- $this->loadMutableFields();
-
- return (int)$this->mDeleted;
+ return $this->mRecord->getVisibility();
}
/**
* Fetch revision content if it's available to the specified audience.
* If the specified audience does not have the ability to view this
- * revision, null will be returned.
+ * revision, or the content could not be loaded, null will be returned.
*
* @param int $audience One of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to $user
* Revision::RAW get the text regardless of permissions
* @param User $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return Content|null
*/
public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return null;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
+ }
+
+ try {
+ return $this->mRecord->getContent( 'main', $audience, $user );
+ }
+ catch ( RevisionAccessException $e ) {
return null;
- } else {
- return $this->getContentInternal();
}
}
* Get original serialized data (without checking view restrictions)
*
* @since 1.21
+ * @deprecated since 1.31, use BlobStore::getBlob instead.
+ *
* @return string
*/
public function getSerializedData() {
- if ( $this->mText === null ) {
- // Revision is immutable. Load on demand.
- $this->mText = $this->loadText();
- }
-
- return $this->mText;
+ $slot = $this->getMainSlotRaw();
+ return $slot->getContent()->serialize();
}
/**
- * Gets the content object for the revision (or null on failure).
- *
- * Note that for mutable Content objects, each call to this method will return a
- * fresh clone.
- *
- * @since 1.21
- * @return Content|null The Revision's content, or null on failure.
- */
- protected function getContentInternal() {
- if ( $this->mContent === null ) {
- $text = $this->getSerializedData();
-
- if ( $text !== null && $text !== false ) {
- // Unserialize content
- $handler = $this->getContentHandler();
- $format = $this->getContentFormat();
-
- $this->mContent = $handler->unserializeContent( $text, $format );
- }
- }
-
- // NOTE: copy() will return $this for immutable content objects
- return $this->mContent ? $this->mContent->copy() : null;
- }
-
- /**
- * Returns the content model for this revision.
+ * Returns the content model for the main slot of this revision.
*
* If no content model was stored in the database, the default content model for the title is
* used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
* is used as a last resort.
*
+ * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
+ *
* @return string The content model id associated with this revision,
* see the CONTENT_MODEL_XXX constants.
*/
public function getContentModel() {
- if ( !$this->mContentModel ) {
- $title = $this->getTitle();
- if ( $title ) {
- $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
- } else {
- $this->mContentModel = CONTENT_MODEL_WIKITEXT;
- }
-
- assert( !empty( $this->mContentModel ) );
- }
-
- return $this->mContentModel;
+ return $this->getMainSlotRaw()->getModel();
}
/**
- * Returns the content format for this revision.
+ * Returns the content format for the main slot of this revision.
*
* If no content format was stored in the database, the default format for this
* revision's content model is returned.
*
+ * @todo: drop this, the format is irrelevant to the revision!
+ *
* @return string The content format id associated with this revision,
* see the CONTENT_FORMAT_XXX constants.
*/
public function getContentFormat() {
- if ( !$this->mContentFormat ) {
- $handler = $this->getContentHandler();
- $this->mContentFormat = $handler->getDefaultFormat();
+ $format = $this->getMainSlotRaw()->getFormat();
- assert( !empty( $this->mContentFormat ) );
+ if ( $format === null ) {
+ // if no format was stored along with the blob, fall back to default format
+ $format = $this->getContentHandler()->getDefaultFormat();
}
- return $this->mContentFormat;
+ return $format;
}
/**
* @return ContentHandler
*/
public function getContentHandler() {
- if ( !$this->mContentHandler ) {
- $model = $this->getContentModel();
- $this->mContentHandler = ContentHandler::getForModelID( $model );
-
- $format = $this->getContentFormat();
-
- if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
- throw new MWException( "Oops, the content format $format is not supported for "
- . "this content model, $model" );
- }
- }
-
- return $this->mContentHandler;
+ return ContentHandler::getForModelID( $this->getContentModel() );
}
/**
* @return string
*/
public function getTimestamp() {
- return wfTimestamp( TS_MW, $this->mTimestamp );
+ return $this->mRecord->getTimestamp();
}
/**
* @return bool
*/
public function isCurrent() {
- return $this->mCurrent;
+ return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
}
/**
* @return Revision|null
*/
public function getPrevious() {
- if ( $this->getTitle() ) {
- $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
- if ( $prev ) {
- return self::newFromTitle( $this->getTitle(), $prev );
- }
- }
- return null;
+ $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* @return Revision|null
*/
public function getNext() {
- if ( $this->getTitle() ) {
- $next = $this->getTitle()->getNextRevisionID( $this->getId() );
- if ( $next ) {
- return self::newFromTitle( $this->getTitle(), $next );
- }
- }
- return null;
- }
-
- /**
- * Get previous revision Id for this page_id
- * This is used to populate rev_parent_id on save
- *
- * @param IDatabase $db
- * @return int
- */
- private function getPreviousRevisionId( $db ) {
- if ( $this->mPage === null ) {
- return 0;
- }
- # Use page_latest if ID is not given
- if ( !$this->mId ) {
- $prevId = $db->selectField( 'page', 'page_latest',
- [ 'page_id' => $this->mPage ],
- __METHOD__ );
- } else {
- $prevId = $db->selectField( 'revision', 'rev_id',
- [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_id DESC' ] );
- }
- return intval( $prevId );
+ $rec = self::getRevisionStore()->getNextRevision( $this->mRecord );
+ return $rec === null ? null : new Revision( $rec );
}
/**
return false;
}
- // Use external methods for external objects, text in table is URL-only then
- if ( in_array( 'external', $flags ) ) {
- $url = $text;
- $parts = explode( '://', $url, 2 );
- if ( count( $parts ) == 1 || $parts[1] == '' ) {
- return false;
- }
-
- if ( isset( $row->old_id ) && $wiki === false ) {
- // Make use of the wiki-local revision text cache
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- // The cached value should be decompressed, so handle that and return here
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
- self::getCacheTTL( $cache ),
- function () use ( $url, $wiki, $flags ) {
- // No negative caching per Revision::loadText()
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
-
- return self::decompressRevisionText( $text, $flags );
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- } else {
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
- }
- }
+ $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
- return self::decompressRevisionText( $text, $flags );
+ return self::getBlobStore()->expandBlob( $text, $flags, $cacheKey );
}
/**
* @return string
*/
public static function compressRevisionText( &$text ) {
- global $wgCompressRevisions;
- $flags = [];
-
- # Revisions not marked this way will be converted
- # on load if $wgLegacyCharset is set in the future.
- $flags[] = 'utf-8';
-
- if ( $wgCompressRevisions ) {
- if ( function_exists( 'gzdeflate' ) ) {
- $deflated = gzdeflate( $text );
-
- if ( $deflated === false ) {
- wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
- } else {
- $text = $deflated;
- $flags[] = 'gzip';
- }
- } else {
- wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
- }
- }
- return implode( ',', $flags );
+ return self::getBlobStore()->compressData( $text );
}
/**
* @return string|bool Decompressed text, or false on failure
*/
public static function decompressRevisionText( $text, $flags ) {
- global $wgLegacyEncoding, $wgContLang;
-
- if ( $text === false ) {
- // Text failed to be fetched; nothing to do
- return false;
- }
-
- if ( in_array( 'gzip', $flags ) ) {
- # Deal with optional compression of archived pages.
- # This can be done periodically via maintenance/compressOld.php, and
- # as pages are saved if $wgCompressRevisions is set.
- $text = gzinflate( $text );
-
- if ( $text === false ) {
- wfLogWarning( __METHOD__ . ': gzinflate() failed' );
- return false;
- }
- }
-
- if ( in_array( 'object', $flags ) ) {
- # Generic compressed storage
- $obj = unserialize( $text );
- if ( !is_object( $obj ) ) {
- // Invalid object
- return false;
- }
- $text = $obj->getText();
- }
-
- if ( $text !== false && $wgLegacyEncoding
- && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
- ) {
- # Old revisions kept around in a legacy encoding?
- # Upconvert on demand.
- # ("utf8" checked for compatibility with some broken
- # conversion scripts 2008-12-30)
- $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
- }
-
- return $text;
+ return self::getBlobStore()->decompressData( $text, $flags );
}
/**
* @return int The revision ID
*/
public function insertOn( $dbw ) {
- global $wgDefaultExternalStore, $wgContentHandlerUseDB;
-
- // We're inserting a new revision, so we have to use master anyway.
- // If it's a null revision, it may have references to rows that
- // are not in the replica yet (the text row).
- $this->mQueryFlags |= self::READ_LATEST;
-
- // Not allowed to have rev_page equal to 0, false, etc.
- if ( !$this->mPage ) {
- $title = $this->getTitle();
- if ( $title instanceof Title ) {
- $titleText = ' for page ' . $title->getPrefixedText();
- } else {
- $titleText = '';
- }
- throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
- }
+ global $wgUser;
- $this->checkContentModel();
+ // Note that $this->mRecord->getId() will typically return null here, but not always,
+ // e.g. not when restoring a revision.
- $data = $this->mText;
- $flags = self::compressRevisionText( $data );
-
- # Write to external storage if required
- if ( $wgDefaultExternalStore ) {
- // Store and get the URL
- $data = ExternalStore::insertToDefault( $data );
- if ( !$data ) {
- throw new MWException( "Unable to store text to external storage" );
- }
- if ( $flags ) {
- $flags .= ',';
- }
- $flags .= 'external';
- }
-
- # Record the text (or external storage URL) to the text table
- if ( $this->mTextId === null ) {
- $dbw->insert( 'text',
- [
- 'old_text' => $data,
- 'old_flags' => $flags,
- ], __METHOD__
- );
- $this->mTextId = $dbw->insertId();
- }
-
- if ( $this->mComment === null ) {
- $this->mComment = "";
- }
-
- # Record the edit in revisions
- $row = [
- 'rev_page' => $this->mPage,
- 'rev_text_id' => $this->mTextId,
- 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
- 'rev_user' => $this->mUser,
- 'rev_user_text' => $this->mUserText,
- 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
- 'rev_deleted' => $this->mDeleted,
- 'rev_len' => $this->mSize,
- 'rev_parent_id' => $this->mParentId === null
- ? $this->getPreviousRevisionId( $dbw )
- : $this->mParentId,
- 'rev_sha1' => $this->mSha1 === null
- ? self::base36Sha1( $this->mText )
- : $this->mSha1,
- ];
- if ( $this->mId !== null ) {
- $row['rev_id'] = $this->mId;
- }
-
- list( $commentFields, $commentCallback ) =
- CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
- $row += $commentFields;
-
- if ( $wgContentHandlerUseDB ) {
- // NOTE: Store null for the default model and format, to save space.
- // XXX: Makes the DB sensitive to changed defaults.
- // Make this behavior optional? Only in miser mode?
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
-
- $title = $this->getTitle();
-
- if ( $title === null ) {
- throw new MWException( "Insufficient information to determine the title of the "
- . "revision's page!" );
+ if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setUser( $wgUser );
+ } else {
+ throw new MWException( 'Cannot insert revision with no associated user.' );
}
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
- $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
- $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
}
- $dbw->insert( 'revision', $row, __METHOD__ );
+ $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
- if ( $this->mId === null ) {
- // Only if auto-increment was used
- $this->mId = $dbw->insertId();
- }
- $commentCallback( $this->mId );
-
- // Assertion to try to catch T92046
- if ( (int)$this->mId === 0 ) {
- throw new UnexpectedValueException(
- 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
- var_export( $row, 1 )
- );
- }
-
- // Insert IP revision into ip_changes for use when querying for a range.
- if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
- $ipcRow = [
- 'ipc_rev_id' => $this->mId,
- 'ipc_rev_timestamp' => $row['rev_timestamp'],
- 'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
- ];
- $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
- }
+ $this->mRecord = $rec;
// Avoid PHP 7.1 warning of passing $this by reference
$revision = $this;
- Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
-
- return $this->mId;
- }
-
- protected function checkContentModel() {
- global $wgContentHandlerUseDB;
-
- // Note: may return null for revisions that have not yet been inserted
- $title = $this->getTitle();
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
- $handler = $this->getContentHandler();
-
- if ( !$handler->isSupportedFormat( $format ) ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use format $format with content model $model on $t" );
- }
+ // TODO: hard-deprecate in 1.32 (or even 1.31?)
+ Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] );
- if ( !$wgContentHandlerUseDB && $title ) {
- // if $wgContentHandlerUseDB is not set,
- // all revisions must use the default content model and format.
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultHandler = ContentHandler::getForModelID( $defaultModel );
- $defaultFormat = $defaultHandler->getDefaultFormat();
-
- if ( $this->getContentModel() != $defaultModel ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't save non-default content model with "
- . "\$wgContentHandlerUseDB disabled: model is $model, "
- . "default for $t is $defaultModel" );
- }
-
- if ( $this->getContentFormat() != $defaultFormat ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use non-default content format with "
- . "\$wgContentHandlerUseDB disabled: format is $format, "
- . "default for $t is $defaultFormat" );
- }
- }
-
- $content = $this->getContent( self::RAW );
- $prefixedDBkey = $title->getPrefixedDBkey();
- $revId = $this->mId;
-
- if ( !$content ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
- );
- }
- if ( !$content->isValid() ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
- );
- }
+ return $rec->getId();
}
/**
* @return string
*/
public static function base36Sha1( $text ) {
- return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
- }
-
- /**
- * Get the text cache TTL
- *
- * @param WANObjectCache $cache
- * @return int
- */
- private static function getCacheTTL( WANObjectCache $cache ) {
- global $wgRevisionCacheExpiry;
-
- if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
- // Do not cache RDBMs blobs in...the RDBMs store
- $ttl = $cache::TTL_UNCACHEABLE;
- } else {
- $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
- }
-
- return $ttl;
- }
-
- /**
- * Lazy-load the revision's text.
- * Currently hardcoded to the 'text' table storage engine.
- *
- * @return string|bool The revision's text, or false on failure
- */
- private function loadText() {
- $cache = ObjectCache::getMainWANInstance();
-
- // No negative caching; negative hits on text rows may be due to corrupted replica DBs
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
- self::getCacheTTL( $cache ),
- function () {
- return $this->fetchText();
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- }
-
- private function fetchText() {
- $textId = $this->getTextId();
-
- // If we kept data for lazy extraction, use it now...
- if ( $this->mTextRow !== null ) {
- $row = $this->mTextRow;
- $this->mTextRow = null;
- } else {
- $row = null;
- }
-
- // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
- // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
- $flags = $this->mQueryFlags;
- $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
- ? self::READ_LATEST_IMMUTABLE
- : 0;
-
- list( $index, $options, $fallbackIndex, $fallbackOptions ) =
- DBAccessObjectUtils::getDBOptions( $flags );
-
- if ( !$row ) {
- // Text data is immutable; check replica DBs first.
- $row = wfGetDB( $index )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $options
- );
- }
-
- // Fallback to DB_MASTER in some cases if the row was not found
- if ( !$row && $fallbackIndex !== null ) {
- // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
- // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
- $row = wfGetDB( $fallbackIndex )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $fallbackOptions
- );
- }
-
- if ( !$row ) {
- wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
- }
-
- $text = self::getRevisionText( $row );
- if ( $row && $text === false ) {
- wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
- }
-
- return is_string( $text ) ? $text : false;
+ return SlotRecord::base36Sha1( $text );
}
/**
* @return Revision|null Revision or null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
- global $wgContentHandlerUseDB;
-
- $fields = [ 'page_latest', 'page_namespace', 'page_title',
- 'rev_text_id', 'rev_len', 'rev_sha1' ];
-
- if ( $wgContentHandlerUseDB ) {
- $fields[] = 'rev_content_model';
- $fields[] = 'rev_content_format';
+ global $wgUser;
+ if ( !$user ) {
+ $user = $wgUser;
}
- $current = $dbw->selectRow(
- [ 'page', 'revision' ],
- $fields,
- [
- 'page_id' => $pageId,
- 'page_latest=rev_id',
- ],
- __METHOD__,
- [ 'FOR UPDATE' ] // T51581
- );
-
- if ( $current ) {
- if ( !$user ) {
- global $wgUser;
- $user = $wgUser;
- }
-
- $row = [
- 'page' => $pageId,
- 'user_text' => $user->getName(),
- 'user' => $user->getId(),
- 'comment' => $summary,
- 'minor_edit' => $minor,
- 'text_id' => $current->rev_text_id,
- 'parent_id' => $current->page_latest,
- 'len' => $current->rev_len,
- 'sha1' => $current->rev_sha1
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $row['content_model'] = $current->rev_content_model;
- $row['content_format'] = $current->rev_content_format;
- }
-
- $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
+ $comment = CommentStoreComment::newUnsavedComment( $summary, null );
- $revision = new Revision( $row );
- } else {
- $revision = null;
- }
+ $title = Title::newFromID( $pageId );
+ $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
- return $revision;
+ return new Revision( $rec );
}
/**
public static function userCanBitfield( $bitfield, $field, User $user = null,
Title $title = null
) {
- if ( $bitfield & $field ) { // aspect is deleted
- if ( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
- if ( $bitfield & self::DELETED_RESTRICTED ) {
- $permissions = [ 'suppressrevision', 'viewsuppressed' ];
- } elseif ( $field & self::DELETED_TEXT ) {
- $permissions = [ 'deletedtext' ];
- } else {
- $permissions = [ 'deletedhistory' ];
- }
- $permissionlist = implode( ', ', $permissions );
- if ( $title === null ) {
- wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
- return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
- } else {
- $text = $title->getPrefixedText();
- wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
- foreach ( $permissions as $perm ) {
- if ( $title->userCan( $perm, $user ) ) {
- return true;
- }
- }
- return false;
- }
- } else {
- return true;
+ global $wgUser;
+
+ if ( !$user ) {
+ $user = $wgUser;
}
+
+ return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
}
/**
* @return string|bool False if not found
*/
static function getTimestampFromId( $title, $id, $flags = 0 ) {
- $db = ( $flags & self::READ_LATEST )
- ? wfGetDB( DB_MASTER )
- : wfGetDB( DB_REPLICA );
- // Casting fix for databases that can't take '' for rev_id
- if ( $id == '' ) {
- $id = 0;
- }
- $conds = [ 'rev_id' => $id ];
- $conds['rev_page'] = $title->getArticleID();
- $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
-
- return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+ return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
}
/**
* @return int
*/
static function countByPageId( $db, $id ) {
- $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
- [ 'rev_page' => $id ], __METHOD__ );
- if ( $row ) {
- return $row->revCount;
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
}
/**
* @return int
*/
static function countByTitle( $db, $title ) {
- $id = $title->getArticleID();
- if ( $id ) {
- return self::countByPageId( $db, $id );
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
}
/**
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
- if ( !$userId ) {
- return false;
- }
-
if ( is_int( $db ) ) {
$db = wfGetDB( $db );
}
- $res = $db->select( 'revision',
- 'rev_user',
- [
- 'rev_page' => $pageId,
- 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
- ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
- foreach ( $res as $row ) {
- if ( $row->rev_user != $userId ) {
- return false;
- }
- }
- return true;
+ return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
}
/**
*
* This method allows for the use of caching, though accessing anything that normally
* requires permission checks (aside from the text) will trigger a small DB lookup.
- * The title will also be lazy loaded, though setTitle() can be used to preload it.
+ * The title will also be loaded if $pageIdOrTitle is an integer ID.
*
- * @param IDatabase $db
- * @param int $pageId Page ID
- * @param int $revId Known current revision of this page
+ * @param IDatabase $db ignored!
+ * @param int|Title $pageIdOrTitle Page ID or Title object
+ * @param int $revId Known current revision of this page. Determined automatically if not given.
* @return Revision|bool Returns false if missing
* @since 1.28
*/
- public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- return $cache->getWithSetCallback(
- // Page/rev IDs passed in from DB to reflect history merges
- $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
- $cache::TTL_WEEK,
- function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
- $setOpts += Database::getCacheSetOptions( $db );
-
- $rev = Revision::loadFromPageId( $db, $pageId, $revId );
- // Reflect revision deletion and user renames
- if ( $rev ) {
- $rev->mTitle = null; // mutable; lazy-load
- $rev->mRefreshMutableFields = true;
- }
-
- return $rev ?: false; // don't cache negatives
- }
- );
- }
-
- /**
- * For cached revisions, make sure the user name and rev_deleted is up-to-date
- */
- private function loadMutableFields() {
- if ( !$this->mRefreshMutableFields ) {
- return; // not needed
- }
+ public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
+ $title = $pageIdOrTitle instanceof Title
+ ? $pageIdOrTitle
+ : Title::newFromID( $pageIdOrTitle );
- $this->mRefreshMutableFields = false;
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- $row = $dbr->selectRow(
- [ 'revision', 'user' ],
- [ 'rev_deleted', 'user_name' ],
- [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
- __METHOD__
- );
- if ( $row ) { // update values
- $this->mDeleted = (int)$row->rev_deleted;
- $this->mUserText = $row->user_name;
- }
+ $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId );
+ return $record ? new Revision( $record ) : false;
}
}
return $factory;
},
+ 'RevisionStore' => function ( MediaWikiServices $services ) {
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = $services->getService( '_SqlBlobStore' );
+
+ $store = new RevisionStore(
+ $services->getDBLoadBalancer(),
+ $blobStore,
+ $services->getMainWANObjectCache()
+ );
+
+ $config = $services->getMainConfig();
+ $store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
+
+ return $store;
+ },
+
+ 'BlobStore' => function ( MediaWikiServices $services ) {
+ return $services->getService( '_SqlBlobStore' );
+ },
+
+ '_SqlBlobStore' => function ( MediaWikiServices $services ) {
+ global $wgContLang; // TODO: manage $wgContLang as a service
+
+ $store = new SqlBlobStore(
+ $services->getDBLoadBalancer(),
+ $services->getMainWANObjectCache()
+ );
+
+ $config = $services->getMainConfig();
+ $store->setCompressRevisions( $config->get( 'CompressRevisions' ) );
+ $store->setCacheExpiry( $config->get( 'RevisionCacheExpiry' ) );
+ $store->setUseExternalStore( $config->get( 'DefaultExternalStore' ) !== false );
+
+ if ( $config->get( 'LegacyEncoding' ) ) {
+ $store->setLegacyEncoding( $config->get( 'LegacyEncoding' ), $wgContLang );
+ }
+
+ return $store;
+ },
+
'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
$config = $services->getMainConfig();
* @return FeedItem
*/
function feedItem( $row ) {
- $rev = new Revision( $row );
- $rev->setTitle( $this->getTitle() );
+ $rev = new Revision( $row, 0, $this->getTitle() );
+
$text = FeedUtils::formatDiffRow(
$this->getTitle(),
$this->getTitle()->getPreviousRevisionID( $rev->getId() ),
*/
function historyLine( $row, $next, $notificationtimestamp = false,
$latest = false, $firstInList = false ) {
- $rev = new Revision( $row );
- $rev->setTitle( $this->getTitle() );
+ $rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
- $prevRev = new Revision( $next );
- $prevRev->setTitle( $this->getTitle() );
+ $prevRev = new Revision( $next, 0, $this->getTitle() );
} else {
$prevRev = null;
}
$params['user'] = $username;
}
} else {
- $target = User::newFromName( $params['user'] );
+ list( $target, $type ) = SpecialBlock::getTargetAndType( $params['user'] );
// T40633 - if the target is a user (not an IP address), but it
// doesn't exist or is unusable, error.
- if ( $target instanceof User &&
- ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) )
+ if ( $type === Block::TYPE_USER &&
+ ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $params['user'] ) )
) {
$this->dieWithError( [ 'nosuchusershort', $params['user'] ], 'nosuchuser' );
}
"Macofe",
"Hamilton Abreu",
"Umherirrender",
- "Fitoschido"
+ "Fitoschido",
+ "Athena in Wonderland"
]
},
"apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentación]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discusión]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anuncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitudes]\n</div>\n<strong>Estado:</strong> Tódalas funcionalidades mostradas nesta páxina deberían estar funcionando, pero a API aínda está desenrolo, e pode ser modificada en calquera momento. Apúntese na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discusión mediawiki-api-announce] para estar informado acerca das actualizacións.\n\n<strong>Solicitudes incorrectas:</strong> Cando se envían solicitudes incorrectas á API, envíase unha cabeceira HTTP coa chave \"MediaWiki-API-Error\" e, a seguir, tanto o valor da cabeceira como o código de erro retornado serán definidos co mesmo valor. Para máis información, consulte [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Erros e avisos]].\n\n<strong>Test:</strong> Para facilitar as probas das peticións da API, consulte [[Special:ApiSandbox]].",
"api-help-param-upload": "Debe ser enviado como un ficheiro importado usando multipart/form-data.",
"api-help-param-multi-separate": "Separe os valores con <kbd>|</kbd> ou [[Special:ApiHelp/main#main/datatypes|outros]].",
"api-help-param-multi-max": "O número máximo de valores é {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} para os bots).",
- "api-help-param-multi-max-simple": "O número máximo de valores é {{PLURAL:1$|1$}}.",
+ "api-help-param-multi-max-simple": "O número máximo de valores é {{PLURAL:$1|$1}}.",
"api-help-param-multi-all": "Para especificar tódolos valores use <kbd>$1</kbd>.",
"api-help-param-default": "Por defecto: $1",
"api-help-param-default-empty": "Por defecto: <span class=\"apihelp-empty\">(baleiro)</span>",
"apihelp-query+logevents-paramvalue-prop-comment": "Legger til kommentaren til loggoppføringen.",
"apihelp-query+logevents-paramvalue-prop-parsedcomment": "Legger til den parsede kommentaren til loggoppføringen.",
"apihelp-query+logevents-paramvalue-prop-details": "Lister opp ekstra detaljer om loggoppføringen.",
+ "apihelp-query+logevents-param-type": "Filtrer loggoppføringer til kun denne typen.",
+ "apihelp-query+logevents-param-action": "Filtrer logghandlinger til kun denne handlingen. Overstyrer <var>$1type</var>. I listen over mulige verdier kan verdier med jokertegnet stjerne, som <kbd>action/*</kbd> ha forskjellige strenger etter skråstreken.",
+ "apihelp-query+logevents-param-user": "Filtrer oppføringer til de som er gjort av den gitte brukeren.",
+ "apihelp-query+logevents-param-title": "Filtrer oppføringer til de som er relatert til ei side.",
+ "apihelp-query+logevents-param-namespace": "Filtrer oppføringer til de i det gitte navnerommet.",
+ "apihelp-query+logevents-param-prefix": "Filtrer oppføringer som starter med dette prefikset.",
+ "apihelp-query+logevents-param-tag": "List bare opp oppføringer som er tagget med denne taggen.",
+ "apihelp-query+logevents-param-limit": "Hvor mange oppføringer som skal returneres.",
+ "apihelp-query+logevents-example-simple": "List opp nylige loggoppføringer.",
+ "apihelp-query+pagepropnames-summary": "List opp alle sideegenskapsnavn i bruk på wikien.",
+ "apihelp-query+pagepropnames-param-limit": "Maksimalt antall navn som skal returneres.",
+ "apihelp-query+pagepropnames-example-simple": "Hent de 10 første egenskapsnavnene.",
+ "apihelp-query+pageprops-summary": "Hent diverse sideegenskaper definert i sideinnholdet.",
+ "apihelp-query+pageprops-param-prop": "List kun opp disse sideegenskapene (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> returnerer sideegenskapsnavn i bruk). Nyttig for å sjekke om sider bruker en viss sideegenskap.",
+ "apihelp-query+pageprops-example-simple": "Hent egenskaper for sidene <kbd>Main Page</kbd> og <kbd>MediaWiki</kbd>.",
+ "apihelp-query+pageswithprop-summary": "Lister opp alle sider med en gitt sideegenskap.",
"apihelp-query+pageswithprop-param-prop": "Hvilken informasjon som skal inkluderes:",
+ "apihelp-query+pageswithprop-paramvalue-prop-ids": "Legger til side-ID-en.",
+ "apihelp-query+pageswithprop-paramvalue-prop-title": "Legger til tittel- og navneroms-ID-en til sida.",
+ "apihelp-query+pageswithprop-paramvalue-prop-value": "Legger til verdien til sideegenskapen.",
+ "apihelp-query+pageswithprop-param-limit": "Maksimalt antall sider som skal returneres.",
+ "apihelp-query+pageswithprop-param-dir": "Hvilken retning det skal sorteres i.",
+ "apihelp-query+pageswithprop-example-simple": "List opp de første 10 sidene som bruker <code>{{DISPLAYTITLE:}}</code>.",
+ "apihelp-query+pageswithprop-example-generator": "Hent ekstra informasjon om de 10 første sidene som bruker <code>__NOTOC__</code>.",
+ "apihelp-query+prefixsearch-summary": "Utfør et prefikssøk for sidetitler.",
+ "apihelp-query+prefixsearch-param-search": "Søkestreng.",
+ "apihelp-query+prefixsearch-param-namespace": "Navnerom det skal søkes i.",
+ "apihelp-query+prefixsearch-param-limit": "Maksimalt antall resultater som skal returneres.",
+ "apihelp-query+prefixsearch-param-offset": "Antall resultater som skal hoppes over.",
+ "apihelp-query+prefixsearch-example-simple": "Søk etter sidetitler som begynner med <kbd>meaning</kbd>.",
+ "apihelp-query+prefixsearch-param-profile": "Søkeprofil som skal brukes.",
+ "apihelp-query+protectedtitles-summary": "List opp alle titler som er beskyttet fra opprettelse.",
+ "apihelp-query+protectedtitles-param-namespace": "List kun opp titler i disse navnerommene.",
+ "apihelp-query+protectedtitles-param-level": "List kun opp titler med disse beskyttelsesnivåene.",
+ "apihelp-query+protectedtitles-param-limit": "Hvor mange sider som skal returneres totalt.",
+ "apihelp-query+protectedtitles-param-prop": "Hvilke egenskaper som skal hentes:",
"apihelp-query+userinfo-param-prop": "Hvilken informasjon som skal inkluderes:",
"apihelp-query+users-param-prop": "Hvilken informasjon som skal inkluderes:",
"apihelp-json-summary": "Resultatdata i JSON-format.",
"apihelp-query+iwbacklinks-param-title": "A hiperligação interwikis a ser procurada. Tem de ser usado em conjunto com <var>$1blprefix</var>.",
"apihelp-query+iwbacklinks-param-limit": "O número total de páginas a serem devolvidas.",
"apihelp-query+iwbacklinks-param-prop": "As propriedades a serem obtidas:",
- "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Adiciona o prefixo do ''link'' interwikis.",
- "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Adiciona o título do ''link'' interwikis.",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "Adiciona o prefixo da hiperligação interwikis.",
+ "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "Adiciona o título da hiperligação interwikis.",
"apihelp-query+iwbacklinks-param-dir": "A direção de listagem.",
"apihelp-query+iwbacklinks-example-simple": "Obter as páginas que contêm ligações para [[wikibooks:Test]].",
"apihelp-query+iwbacklinks-example-generator": "Obter informação sobre as páginas que contêm ligações para [[wikibooks:Test]].",
"apihelp-query+iwlinks-param-title": "Hiperligação interwikis a ser procurada. Tem de ser usado em conjunto com <var>$1prefix</var>.",
"apihelp-query+iwlinks-param-dir": "A direção de listagem.",
"apihelp-query+iwlinks-example-simple": "Obter os ''links'' interwikis da página <kbd>Main Page</kbd>.",
- "apihelp-query+langbacklinks-summary": "Encontrar todas as páginas que contêm ''links'' para o ''link'' interlínguas indicado.",
+ "apihelp-query+langbacklinks-summary": "Encontrar todas as páginas que contêm hiperligações para a hiperligação interlínguas indicada.",
"apihelp-query+langbacklinks-extended-description": "Pode ser usado para encontrar todos os ''links'' para um determinado código de língua, ou todos os ''links'' para um determinado título (de uma língua). Se nenhum for usado, isso efetivamente significa \"todos os ''links'' interlínguas\".\n\nNote que os ''links'' interlínguas adicionados por extensões podem não ser considerados.",
"apihelp-query+langbacklinks-param-lang": "A língua da hiperligação da língua.",
"apihelp-query+langbacklinks-param-title": "Hiperligação interlínguas a ser procurada. Tem de ser usado com $1lang.",
"apihelp-query+langbacklinks-param-limit": "O número total de páginas a serem devolvidas.",
"apihelp-query+langbacklinks-param-prop": "As propriedades a serem obtidas:",
"apihelp-query+langbacklinks-paramvalue-prop-lllang": "Adiciona o código de língua da ligação interlínguas.",
- "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Adiciona o título do ''link'' interlínguas.",
+ "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "Adiciona o título da hiperligação interlínguas.",
"apihelp-query+langbacklinks-param-dir": "A direção de listagem.",
"apihelp-query+langbacklinks-example-simple": "Obter as páginas que contêm ligações para [[:fr:Test]].",
"apihelp-query+langbacklinks-example-generator": "Obter informações sobre as páginas que contêm ligações para [[:fr:Test]].",
"apihelp-imagerotate-param-tags": "Изменить метки записи в журнале загрузок.",
"apihelp-imagerotate-example-simple": "Повернуть <kbd>File:Example.png</kbd> на <kbd>90</kbd> градусов.",
"apihelp-imagerotate-example-generator": "Повернуть все изображения в <kbd>Category:Flip</kbd> на <kbd>180</kbd> градусов.",
- "apihelp-import-summary": "Импорт страницы из другой вики, или из XML-файла.",
+ "apihelp-import-summary": "Импорт страницы из другой вики или XML-файла.",
"apihelp-import-extended-description": "Обратите внимание, что HTTP POST-запрос должен быть осуществлён как загрузка файла (то есть, с использованием многотомных данных) при отправки файла через параметр <var>xml</var>.",
"apihelp-import-param-summary": "Описание записи журнала импорта.",
"apihelp-import-param-xml": "Загруженный XML-файл.",
+ "apihelp-import-param-interwikiprefix": "Для загруженных импортов: префикс интервики для неизвестных имён участников (а также известных, если задан <var>$1assignknownusers</var>).",
+ "apihelp-import-param-assignknownusers": "Связать правки с локальными участниками, когда участники с такими именами существуют.",
"apihelp-import-param-interwikisource": "Для импорта из других вики: импортируемая вики.",
"apihelp-import-param-interwikipage": "Для импорта из других вики: импортируемая страница.",
"apihelp-import-param-fullhistory": "Для импорта из других вики: импортировать полную историю, а не только текущую страницу.",
"apihelp-linkaccount-summary": "Связать аккаунт третьей стороны с текущим участником.",
"apihelp-linkaccount-example-link": "Начать связывание аккаунта с <kbd>Example</kdb>.",
"apihelp-login-summary": "Вход и получение аутентификационных cookie.",
- "apihelp-login-extended-description": "Это действие должно быть использовано только в комбинации со [[Special:BotPasswords]]; использование этого модуля для входа в основной аккаунт не поддерживается и может сбиться без предупреждения. Для безопасного входа в основной аккаунт, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+ "apihelp-login-extended-description": "Это действие должно быть использовано только в комбинации со [[Special:BotPasswords]]; использование этого модуля для входа в основной аккаунт устарело и может сбиться без предупреждения. Для безопасного входа в основной аккаунт, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
"apihelp-login-extended-description-nobotpasswords": "Это действие не поддерживается и может сбиться без предупреждения. Для безопасного входа, используйте <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
"apihelp-login-param-name": "Имя участника.",
"apihelp-login-param-password": "Пароль.",
"apihelp-query+allrevisions-param-generatetitles": "При использовании в качестве генератора, генерирует названия страниц вместо идентификаторов версий.",
"apihelp-query+allrevisions-example-user": "Перечислить последние 50 правок участника <kbd>Example</kbd>.",
"apihelp-query+allrevisions-example-ns-main": "Перечислить первые 50 правок в основном пространстве.",
- "apihelp-query+mystashedfiles-summary": "Получить список файлов в тайнике (upload stash) текущего участника.",
+ "apihelp-query+mystashedfiles-summary": "Получить список файлов во временном хранилище текущего участника.",
"apihelp-query+mystashedfiles-param-prop": "Какие свойства файлов запрашивать.",
"apihelp-query+mystashedfiles-paramvalue-prop-size": "Запросить размер и разрешение изображения.",
"apihelp-query+mystashedfiles-paramvalue-prop-type": "Запросить MIME- и медиа-тип файла.",
"apihelp-query+mystashedfiles-param-limit": "Сколько файлов получить.",
- "apihelp-query+mystashedfiles-example-simple": "Получить ключ, размер и разрешение файлов в тайнике текущего участника.",
+ "apihelp-query+mystashedfiles-example-simple": "Получить ключ, размер и разрешение файлов во временном хранилище текущего участника.",
"apihelp-query+alltransclusions-summary": "Перечисление всех включений (страниц, вставленных с помощью {{x}}), включая несуществующие.",
"apihelp-query+alltransclusions-param-from": "Название включения, с которого начать перечисление.",
"apihelp-query+alltransclusions-param-to": "Название включения, на котором закончить перечисление.",
"apihelp-query+deletedrevs-param-excludeuser": "Не перечислять правки данного участника.",
"apihelp-query+deletedrevs-param-namespace": "Перечислять только страницы этого пространства имён.",
"apihelp-query+deletedrevs-param-limit": "Максимальное количество правок в списке.",
- "apihelp-query+deletedrevs-param-prop": "Ð\9aакие Ñ\81войÑ\81Ñ\82ва возвÑ\80аÑ\89аÑ\82Ñ\8c:\n;revid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 Ñ\83далÑ\91нной пÑ\80авки.\n;parentid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 пÑ\80едÑ\8bдÑ\83Ñ\89ей веÑ\80Ñ\81ии Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b.\n;user: Ð\94обавлÑ\8fеÑ\82 ник Ñ\83Ñ\87аÑ\81Ñ\82ника, Ñ\81делавÑ\88его пÑ\80авкÑ\83.\n;userid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 Ñ\83Ñ\87аÑ\81Ñ\82ника, Ñ\81делавÑ\88его пÑ\80авкÑ\83.\n;comment: Ð\94обавлÑ\8fеÑ\82 опиÑ\81ание пÑ\80авки.\n;parsedcomment: Ð\94обавлÑ\8fеÑ\82 Ñ\80аÑ\81паÑ\80Ñ\81енное опиÑ\81ание пÑ\80авки.\n;minor: Ð\9eÑ\82меÑ\87аеÑ\82, бÑ\8bла ли пÑ\80авка малÑ\8bм.\n;len: Ð\94обавлÑ\8fеÑ\82 длинÑ\83 (в байÑ\82аÑ\85) пÑ\80авки.\n;sha1: Ð\94обавлÑ\8fеÑ\82 Ñ\85Ñ\8dÑ\88 SHA-1 (base 16) пÑ\80авки.\n;content: Ð\94обавлÑ\8fеÑ\82 Ñ\81одеÑ\80жимое пÑ\80авки.\n;token: <span class=\"apihelp-deprecated\">Ð\9dе поддеÑ\80живаеÑ\82Ñ\81Ñ\8f.</span> Возвращает токен редактирования.\n;tags: Теги правки.",
+ "apihelp-query+deletedrevs-param-prop": "Ð\9aакие Ñ\81войÑ\81Ñ\82ва возвÑ\80аÑ\89аÑ\82Ñ\8c:\n;revid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 Ñ\83далÑ\91нной пÑ\80авки.\n;parentid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 пÑ\80едÑ\8bдÑ\83Ñ\89ей веÑ\80Ñ\81ии Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b.\n;user: Ð\94обавлÑ\8fеÑ\82 ник Ñ\83Ñ\87аÑ\81Ñ\82ника, Ñ\81делавÑ\88его пÑ\80авкÑ\83.\n;userid: Ð\94обавлÑ\8fеÑ\82 иденÑ\82иÑ\84икаÑ\82оÑ\80 Ñ\83Ñ\87аÑ\81Ñ\82ника, Ñ\81делавÑ\88его пÑ\80авкÑ\83.\n;comment: Ð\94обавлÑ\8fеÑ\82 опиÑ\81ание пÑ\80авки.\n;parsedcomment: Ð\94обавлÑ\8fеÑ\82 Ñ\80аÑ\81паÑ\80Ñ\81енное опиÑ\81ание пÑ\80авки.\n;minor: Ð\9eÑ\82меÑ\87аеÑ\82, бÑ\8bла ли пÑ\80авка малÑ\8bм.\n;len: Ð\94обавлÑ\8fеÑ\82 длинÑ\83 (в байÑ\82аÑ\85) пÑ\80авки.\n;sha1: Ð\94обавлÑ\8fеÑ\82 Ñ\85Ñ\8dÑ\88 SHA-1 (base 16) пÑ\80авки.\n;content: Ð\94обавлÑ\8fеÑ\82 Ñ\81одеÑ\80жимое пÑ\80авки.\n;token: <span class=\"apihelp-deprecated\">УÑ\81Ñ\82аÑ\80ело.</span> Возвращает токен редактирования.\n;tags: Теги правки.",
"apihelp-query+deletedrevs-example-mode1": "Список последних удалённых правок страниц <kbd>Main Page</kbd> и <kbd>Talk:Main Page</kbd> с содержимым (режим 1).",
"apihelp-query+deletedrevs-example-mode2": "Список последних 50 удалённых правок участника <kbd>Bob</kbd> (режим 2).",
"apihelp-query+deletedrevs-example-mode3-main": "Список последних 50 удалённых правок в основном пространстве имён (режим 3)",
"apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "Распарсенное описание правки.",
"apihelp-query+revisions+base-paramvalue-prop-content": "Текст версии.",
"apihelp-query+revisions+base-paramvalue-prop-tags": "Метки версии.",
- "apihelp-query+revisions+base-paramvalue-prop-parsetree": "<span class=\"apihelp-deprecated\">Ð\9dе поддеÑ\80живаеÑ\82Ñ\81Ñ\8f.</span> Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Дерево парсинга XML содержимого версии (требуется модель содержимого <code>$1</code>).",
+ "apihelp-query+revisions+base-paramvalue-prop-parsetree": "<span class=\"apihelp-deprecated\">УÑ\81Ñ\82аÑ\80ело.</span> Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Дерево парсинга XML содержимого версии (требуется модель содержимого <code>$1</code>).",
"apihelp-query+revisions+base-param-limit": "Сколько версий вернуть.",
"apihelp-query+revisions+base-param-expandtemplates": "Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd>. Раскрыть шаблоны в содержимом версии (требуется $1prop=content).",
"apihelp-query+revisions+base-param-generatexml": "Вместо этого используйте <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> или <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>. Сгенерировать дерево парсинга XML содержимого версии (требуется $1prop=content).",
"apihelp-query+search-paramvalue-prop-sectiontitle": "Добавляет заголовок найденного раздела.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Добавляет распарсенный фрагмент найденной категории.",
"apihelp-query+search-paramvalue-prop-isfilematch": "Добавляет логическое значение, обозначающее, удовлетворяет ли поисковому запросу содержимое файла.",
+ "apihelp-query+search-paramvalue-prop-extensiondata": "Добавляет дополнительные данные, сгенерированные расширениями.",
"apihelp-query+search-paramvalue-prop-score": "Игнорируется.",
"apihelp-query+search-paramvalue-prop-hasrelated": "Игнорируется.",
"apihelp-query+search-param-limit": "Сколько страниц вернуть.",
"apihelp-query+siteinfo-example-simple": "Запросить информацию о сайте.",
"apihelp-query+siteinfo-example-interwiki": "Запросить список локальных префиксов интервик.",
"apihelp-query+siteinfo-example-replag": "Проверить текущее отставание репликации.",
- "apihelp-query+stashimageinfo-summary": "Возвращает информацию о файлах в тайнике (upload stash).",
+ "apihelp-query+stashimageinfo-summary": "Возвращает информацию о файлах во временном хранилище.",
"apihelp-query+stashimageinfo-param-filekey": "Ключ, идентифицирующий предыдущую временную загрузку.",
"apihelp-query+stashimageinfo-param-sessionkey": "Синоним $1filekey для обратной совместимости.",
- "apihelp-query+stashimageinfo-example-simple": "Вернуть информацию о файле в тайнике.",
+ "apihelp-query+stashimageinfo-example-simple": "Вернуть информацию о файле во временном хранилище.",
"apihelp-query+stashimageinfo-example-params": "Вернуть эскизы двух файлов в тайнике.",
"apihelp-query+tags-summary": "Список меток правок.",
"apihelp-query+tags-param-limit": "Максимальное количество меток в списке.",
"apihelp-tag-example-rev": "Добавить метку <kbd>vandalism</kbd> к версии с идентификатором 123 без указания причины.",
"apihelp-tag-example-log": "Удаление метки <kbd>spam</kbd> из записи журнала с идентификатором 123 с причиной <kbd>Wrongly applied</kbd>.",
"apihelp-tokens-summary": "Получение токенов для действий, связанных с редактированием данных.",
- "apihelp-tokens-extended-description": "Этот модуль не поддерживается в пользу [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
+ "apihelp-tokens-extended-description": "Этот модуль устарел в пользу [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-tokens-param-type": "Типы запрашиваемых токенов.",
"apihelp-tokens-example-edit": "Получить токен редактирования (по умолчанию).",
"apihelp-tokens-example-emailmove": "Получить токен электронной почты и переименования.",
"apihelp-upload-param-url": "Ссылка на запрашиваемый файл.",
"apihelp-upload-param-filekey": "Ключ, идентифицирующий предыдущую временную загрузку.",
"apihelp-upload-param-sessionkey": "Синоним $1filekey, обслуживаемый для обратной совместимости.",
- "apihelp-upload-param-stash": "Ð\95Ñ\81ли задано, Ñ\81еÑ\80веÑ\80 вÑ\80еменно помеÑ\81Ñ\82иÑ\82 Ñ\84айл в Ñ\82айник вмеÑ\81Ñ\82о загÑ\80Ñ\83зки его в Ñ\85Ñ\80анилиÑ\89е.",
+ "apihelp-upload-param-stash": "Ð\95Ñ\81ли задано, Ñ\81еÑ\80веÑ\80 помеÑ\81Ñ\82иÑ\82 Ñ\84айл во вÑ\80еменное Ñ\85Ñ\80анилиÑ\89е, не добавив в поÑ\81Ñ\82оÑ\8fнное.",
"apihelp-upload-param-filesize": "Полны размер файла.",
"apihelp-upload-param-offset": "Смещение блока в байтах.",
"apihelp-upload-param-chunk": "Содержимое кусочка.",
"api-help-lead": "Это автоматически сгенерированная страница документации MediaWiki API.\n\nДокументация и примеры: https://www.mediawiki.org/wiki/API",
"api-help-main-header": "Главный модуль",
"api-help-undocumented-module": "Нет документации для модуля $1.",
- "api-help-flag-deprecated": "Этот модуль не поддерживается.",
+ "api-help-flag-deprecated": "Этот модуль устарел.",
"api-help-flag-internal": "<strong>Этот модуль внутренний или нестабильный.</strong> Его операции могут измениться без предупреждения.",
"api-help-flag-readrights": "Этот модуль требует прав на чтение.",
"api-help-flag-writerights": "Этот модуль требует прав на запись.",
"api-help-license-noname": "Лицензия: [[$1|см. ссылку]]",
"api-help-license-unknown": "Лицензия: <span class=\"apihelp-unknown\">unknown</span>",
"api-help-parameters": "Параметр{{PLURAL:$1||ы}}:",
- "api-help-param-deprecated": "Ð\9dе поддеÑ\80живаеÑ\82Ñ\81Ñ\8f.",
+ "api-help-param-deprecated": "УÑ\81Ñ\82аÑ\80ело.",
"api-help-param-required": "Это обязательный параметр.",
"api-help-datatypes-header": "Типы данных",
"api-help-datatypes": "Ввод в MediaWiki должен быть NFC-нормализованным UTF-8. MediaWiki может попытаться преобразовать другой ввод, но это приведёт к провалу некоторых операций (таких, как [[Special:ApiHelp/edit|редактирование]] со сверкой MD5).\n\nНекоторые типы параметров в запросах API требуют дополнительных пояснений:\n;логический\n:Логические параметры работают как флажки (checkboxes) в HTML: если параметр задан, независимо от его значения, он воспринимается за истину. Для передачи ложного значения просто опустите параметр.\n;временные метки\n:Временные метки могут быть заданы в нескольких форматах. Рекомендуемым является дата и время ISO 8601. Всё время считается в UTC, любые включённые часовые пояса игнорируются.\n:* Дата и время ISO 8601: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (знаки препинания и <kbd>Z</kbd> необязательны)\n:* Дата и время ISO 8601 с (игнорируемой) дробной частью секунд: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (дефисы, двоеточия и <kbd>Z</kbd> необязательны)\n:* Формат MediaWiki: <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Общий числовой формат: <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (необязательный часовой пояс <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd> или <kbd>-<var>##</var></kbd> игнорируется)\n:* Формат EXIF: <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат RFC 2822 (часовой пояс может быть опущен): <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат RFC 850 (часовой пояс может быть опущен): <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Формат ctime языка программирования C: <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Количество секунд, прошедших с 1970-01-01T00:00:00Z, в виде челого числа с от 1 до 13 знаками (исключая <kbd>0</kbd>)\n:* Строка <kbd>now</kbd>\n;альтернативный разделитель значений\n:Параметры, принимающие несколько значений, обычно отправляются со значениями, разделёнными с помощью символа пайпа, например, <kbd>param=value1|value2</kbd> или <kbd>param=value1%7Cvalue2</kbd>. Если значение должно содержать символ пайпа, используйте U+001F (Unit Separator) в качестве разделителя ''и'' добавьте в начало значения U+001F, например, <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
"api-help-param-direction": "В каком порядке перечислять:\n;newer: Начать с самых старых. Обратите внимание: $1start должно быть раньше $1end.\n;older: Начать с самых новых (по умолчанию). Обратите внимание: $1start должно быть позже $1end.",
"api-help-param-continue": "Когда доступно больше результатов, используйте это для продолжения.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(описание отсутствует)</span>",
+ "api-help-param-maxbytes": "Не может быть длиннее $1 {{PLURAL:$1|байта|байтов}}.",
+ "api-help-param-maxchars": "Не может быть длиннее $1 {{PLURAL:$1|символа|символов}}.",
"api-help-examples": "Пример{{PLURAL:$1||ы}}:",
"api-help-permissions": "{{PLURAL:$1|Разрешение|Разрешения}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Гарантируется}}: $2",
"apierror-blockedfrommail": "Отправка электронной почты была для вас заблокирована.",
"apierror-blocked": "Редактирование было для вас заблокировано.",
"apierror-botsnotsupported": "Этот интерфейс не поддерживается для ботов.",
- "apierror-cannot-async-upload-file": "Параметры <var>async</var> и <var>file</var> не могут применяться вместе. Если вы хотите ассинхронно обработать загруженный файл, сначала загрузите его в тайник (используя параметр <var>stash</var>), а затем опубликуйте этот файл ассинхронно (используя параметры <var>filekey</var> и <var>async</var>).",
+ "apierror-cannot-async-upload-file": "Параметры <var>async</var> и <var>file</var> не могут применяться вместе. Если вы хотите ассинхронно обработать загруженный файл, сначала загрузите его во временное хранилище (используя параметр <var>stash</var>), а затем опубликуйте этот файл ассинхронно (используя параметры <var>filekey</var> и <var>async</var>).",
"apierror-cannotreauthenticate": "Это действие недоступно, так как ваша личность не может быть подтверждена.",
"apierror-cannotviewtitle": "У вас нет прав на просмотр $1.",
"apierror-cantblock-email": "У вас нет прав блокировать участникам отправку электронной почты через интерфейс вики.",
"apierror-invalidurlparam": "Некорректное значение <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Некорректное имя участника «$1».",
"apierror-invaliduserid": "Некорректный идентификатор участника <var>$1</var>.",
+ "apierror-maxbytes": "Параметр <var>$1</var> не может быть длиннее $2 {{PLURAL:$2|байта|байтов}}",
+ "apierror-maxchars": "Параметр <var>$1</var> не может быть длиннее $2 {{PLURAL:$2|символа|символов}}",
"apierror-maxlag-generic": "Ожидание сервера базы данных: $1 {{PLURAL:$1|секунда|секунды|секунд}} задержки.",
"apierror-maxlag": "Ожидание $2: $1 {{PLURAL:$1|секунда|секунды|секунд}} задержки.",
"apierror-mimesearchdisabled": "Поиск по MIME отключен в жадном режиме.",
"apierror-mustbeloggedin-generic": "Вы должны быть авторизованы.",
"apierror-mustbeloggedin-linkaccounts": "Вы должны быть авторизованы для привязывания аккаунтов.",
"apierror-mustbeloggedin-removeauth": "Вы должны быть авторизованы для удаления аутентификационных данных.",
- "apierror-mustbeloggedin-uploadstash": "Тайник загÑ\80Ñ\83зки (upload stash) доÑ\81Ñ\82Ñ\83пен только для авторизованных участников.",
+ "apierror-mustbeloggedin-uploadstash": "Ð\92Ñ\80еменное Ñ\85Ñ\80анилиÑ\89е доÑ\81Ñ\82Ñ\83пно только для авторизованных участников.",
"apierror-mustbeloggedin": "Вы должны быть авторизованы в $1.",
"apierror-mustbeposted": "Модуль <kbd>$1</kbd> требует запроса POST.",
"apierror-mustpostparams": "{{PLURAL:$2|Следующий параметр был найден|Следующие параметры были найдены}} в строке запроса, но {{PLURAL:$2|должен|должны}} находиться в теле POST: $1.",
"apierror-sizediffdisabled": "Подсчёт разницы размеров отключён в жадном режиме.",
"apierror-spamdetected": "Ваша правка была отклонена, так как содержит спам: <code>$1</code>.",
"apierror-specialpage-cantexecute": "У вас нет прав, чтобы просматривать результаты этой служебной страницы.",
- "apierror-stashedfilenotfound": "Невозможно найти файл в тайнике: $1.",
+ "apierror-stashedfilenotfound": "Невозможно найти файл во временном хранилище: $1.",
"apierror-stashedit-missingtext": "Не найдено содержимого тайника для данного хэша.",
"apierror-stashfailed-complete": "Загрузка по кусочкам уже завершена, проверьте статус для получения подробной информации.",
"apierror-stashfailed-nosession": "Не найдено сессии загрузки по кусочкам с заданным ключом.",
- "apierror-stashfilestorage": "Невозможно сохранить загрузку в тайник: $1",
+ "apierror-stashfilestorage": "Невозможно сохранить файл во временном хранилище: $1",
"apierror-stashinvalidfile": "Некорректный файл в тайнике.",
"apierror-stashnosuchfilekey": "Нет такого ключа файла: $1.",
"apierror-stashpathinvalid": "Ключ файла относится к некорректному формату или сам некорректен: $1.",
"apierror-stashwrongowner": "Некорректный владелец: $1",
- "apierror-stashzerolength": "Файл имеет нулевую длину и не может быть сохранён в тайник: $1",
+ "apierror-stashzerolength": "Файл имеет нулевую длину и не может быть сохранён во временное хранилище: $1",
"apierror-systemblocked": "Вы были заблокированы автоматически MediaWiki.",
"apierror-templateexpansion-notwikitext": "Раскрытие шаблонов разрешено только для вики-текстового содержимого. $1 использует модель содержимого $2.",
"apierror-timeout": "Сервер не ответил за ожидаемое время.",
"apierror-unsupportedrepo": "Локальное хранилище файлов не поддерживает запрос всех изображений.",
"apierror-upload-filekeyneeded": "Необходимо задать <var>filekey</var>, если <var>offset</var> не ноль.",
"apierror-upload-filekeynotallowed": "Невозможно обработать <var>filekey</var>, если <var>offset</var> равен 0.",
- "apierror-upload-inprogress": "Процесс загрузки из тайника уже запущен.",
+ "apierror-upload-inprogress": "Процесс загрузки из временного хранилища уже запущен.",
"apierror-upload-missingresult": "Нет результатов данных статуса.",
"apierror-urlparamnormal": "Невозможно нормализовать параметры изображения для $1.",
"apierror-writeapidenied": "У вас нет прав на редактирование этой вики через API.",
"apiwarn-badutf8": "Значение, переданное <var>$1</var>, содержит некорректные или ненормализованные данные. Текстовые данные должны быть корректным NFC-нормализованным Юникодом без символов управления C0, кроме HT (\\t), LF (\\n) и CR (\\r).",
"apiwarn-checktoken-percentencoding": "Проверьте, что символы вроде «+» в токене корректно закодированы %-последовательностями в ссылке.",
"apiwarn-compare-nocontentmodel": "Модель содержимого не может быть определена, предполагается $1.",
- "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> не поддерживается. Пожалуйста, вместо него используйте <kbd>prop=deletedrevisions</kbd> или <kbd>list=alldeletedrevisions</kbd>.",
+ "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> устарел. Пожалуйста, вместо него используйте <kbd>prop=deletedrevisions</kbd> или <kbd>list=alldeletedrevisions</kbd>.",
"apiwarn-deprecation-expandtemplates-prop": "Поскольку никакие значения не были указаны в параметре <var>prop</var>, был использован наследованный формат. Этот формат является устаревшим, и в будущем параметру <var>prop</var> будет присвоено значение по умолчанию, что приведёт к повсеместному использованию нового формата.",
"apiwarn-deprecation-httpsexpected": "Использован HTTP, где ожидался HTTPS.",
- "apiwarn-deprecation-login-botpw": "Вход в основной аккаунт через <kbd>action=login</kbd> не поддерживается и может быть отключен без предупреждения. Для продолжения авторизации с <kbd>action=login</kbd>, см.\n[[Special:BotPasswords]]. Для безопасного продолжения использования входа в основной аккаунт, см. <kbd>action=clientlogin</kbd>.",
+ "apiwarn-deprecation-login-botpw": "Вход в основной аккаунт через <kbd>action=login</kbd> устарел и может быть отключен без предупреждения. Для продолжения авторизации с <kbd>action=login</kbd>, см.\n[[Special:BotPasswords]]. Для безопасного продолжения использования входа в основной аккаунт, см. <kbd>action=clientlogin</kbd>.",
"apiwarn-deprecation-login-nobotpw": "Вход в основной аккаунт через <kbd>action=login</kbd> не поддерживается и может быть отключен без предупреждения. Для безопасной авторизации, см. <kbd>action=clientlogin</kbd>.",
- "apiwarn-deprecation-login-token": "Запрос токена через <kbd>action=login</kbd> не поддерживается. Вместо этого, см. <kbd>action=query&meta=tokens&type=login</kbd>.",
+ "apiwarn-deprecation-login-token": "Запрос токена через <kbd>action=login</kbd> устарел. Используйте <kbd>action=query&meta=tokens&type=login</kbd>.",
"apiwarn-deprecation-parameter": "Параметр <var>$1</var> не поддерживается.",
- "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> не поддерживается с MediaWiki 1.28. Используйте <kbd>prop=headhtml</kbd> при создании новых HTML документов, или <kbd>prop=modules|jsconfigvars</kbd> при обновлении документов на стороне клиента.",
- "apiwarn-deprecation-purge-get": "Использование <kbd>action=purge</kbd> посредством GET не поддерживается. Используйте POST.",
+ "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> устарело с MediaWiki 1.28. Используйте <kbd>prop=headhtml</kbd> при создании новых HTML документов или <kbd>prop=modules|jsconfigvars</kbd> при обновлении документов на стороне клиента.",
+ "apiwarn-deprecation-purge-get": "Использование <kbd>action=purge</kbd> посредством GET устарело. Используйте POST.",
"apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> не поддерживается. Пожалуйста, используйте <kbd>$2</kbd>.",
"apiwarn-difftohidden": "Невозможно сравнить с r$1: содержимое скрыто.",
"apiwarn-errorprinterfailed": "Сборщик ошибок упал. Будет совершена повторная попытка без параметров.",
"apiwarn-tokens-origin": "Токены не могут быть получены, пока не применено правило ограничения домена.",
"apiwarn-toomanyvalues": "Слишком много значений передано параметру <var>$1</var>. Максимальное число — $2.",
"apiwarn-truncatedresult": "Результат был усечён, поскольку в противном случае он был бы больше лимита в $1 {{PLURAL:$1|байт|байта|байт}}.",
- "apiwarn-unclearnowtimestamp": "Передача «$2» в качестве параметра временной метки <var>$1</var> не поддерживается. Если по какой-то причине вы хотите прямо указать текущее время без вычисления его на стороне клиента, используйте <kbd>now</kbd>.",
+ "apiwarn-unclearnowtimestamp": "Передача «$2» в качестве параметра временной метки <var>$1</var> устарело. Если по какой-то причине вы хотите прямо указать текущее время без вычисления его на стороне клиента, используйте <kbd>now</kbd>.",
"apiwarn-unrecognizedvalues": "{{PLURAL:$3|Нераспознанное значение|Нераспознанные значения}} параметра <var>$1</var>: $2.",
"apiwarn-unsupportedarray": "Параметр <var>$1</var> использует неподдерживаемый синтаксис массивов PHP.",
"apiwarn-urlparamwidth": "Значение ширины ($2), переданное в <var>$1urlparam</var>, было проигнорировано в пользу значения ($3), полученного из параметров <var>$1urlwidth</var>/<var>$1urlheight</var>.",
if ( $titleObj->getLatestRevID() ) {
$revision = Revision::newKnownCurrent(
$dbr,
- $titleObj->getArticleID(),
- $titleObj->getLatestRevID()
+ $titleObj
);
} else {
$revision = false;
use MediaWiki\Edit\PreparedEdit;
use \MediaWiki\Logger\LoggerFactory;
use \MediaWiki\MediaWikiServices;
+use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBError;
$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
} else {
$dbr = wfGetDB( DB_REPLICA );
- $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
+ $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
}
if ( $revision ) { // sanity
$conditions['page_latest'] = $lastRevision;
}
+ $revId = $revision->getId();
+ Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
+
$row = [ /* SET */
- 'page_latest' => $revision->getId(),
+ 'page_latest' => $revId,
'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
'page_is_redirect' => $rt !== null ? 1 : 0,
* @return Revision|bool False if missing
*/
public static function statelessFetchRevision( Title $title, $parser = false ) {
- $pageId = $title->getArticleID();
- $revId = $title->getLatestRevID();
-
- $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
- if ( $rev ) {
- $rev->setTitle( $title );
- }
+ $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
return $rev;
}
* @return Content|null
*/
protected function getContentObj( Title $title ) {
- $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title->getArticleID(),
- $title->getLatestRevID() );
+ $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
if ( !$revision ) {
return null;
}
- $revision->setTitle( $title );
$content = $revision->getContent( Revision::RAW );
if ( !$content ) {
wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
public function execute( $subpage ) {
$this->rcSubpage = $subpage;
- $this->considerActionsForDefaultSavedQuery();
+ $this->considerActionsForDefaultSavedQuery( $subpage );
$opts = $this->getOptions();
try {
* Check whether or not the page should load defaults, and if so, whether
* a default saved query is relevant to be redirected to. If it is relevant,
* redirect properly with all necessary query parameters.
+ *
+ * @param string $subpage
*/
- protected function considerActionsForDefaultSavedQuery() {
+ protected function considerActionsForDefaultSavedQuery( $subpage ) {
if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
return;
}
// but are still valid and requested in the URL
$query = array_merge( $this->getRequest()->getValues(), $query );
unset( $query[ 'title' ] );
- $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
+ $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
} else {
// There's a default, but the version is not 2, and the server can't
// actually recognize the query itself. This happens if it is before
/**
* @param stdClass $result Result row from recent changes
- * @return Revision|bool
+ * @param Title $title
+ * @return bool|Revision
*/
- protected function revisionFromRcResult( stdClass $result ) {
+ protected function revisionFromRcResult( stdClass $result, Title $title ) {
return new Revision( [
'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
'deleted' => $result->rc_deleted,
'user_text' => $result->rc_user_text,
'user' => $result->rc_user,
- ] );
+ ], 0, $title );
}
/**
// Revision deletion works on revisions,
// so cast our recent change row to a revision row.
- $rev = $this->revisionFromRcResult( $result );
- $rev->setTitle( $title );
+ $rev = $this->revisionFromRcResult( $result, $title );
$classes = [];
$attribs = [ 'data-mw-revid' => $result->rev_id ];
/** @var bool|Title */
protected $rclTargetTitle;
- protected $rclTarget;
-
function __construct() {
parent::__construct( 'Recentchangeslinked' );
}
public function parseParameters( $par, FormOptions $opts ) {
$opts['target'] = $par;
- $this->rclTarget = $par;
}
/**
return $this->prefixSearchString( $search, $limit, $offset );
}
- /**
- * Get a self-referential title object
- * with consideration to the given subpage.
- *
- * @param string|bool $subpage
- * @return Title
- * @since 1.23
- */
- public function getPageTitle( $subpage = false ) {
- $subpage = $subpage ? $subpage : $this->rclTarget;
-
- return parent::getPageTitle( $subpage );
- }
-
protected function outputNoResults() {
if ( $this->getTargetTitle() === false ) {
$this->getOutput()->addHTML(
"imported-log-entries": "{{PLURAL:$1|Імпартаваны $1 запіс журнала|Імпартаваныя $1 запісы журнала|Імпартаваныя $1 запісаў журнала}}.",
"importfailed": "Немагчыма імпартаваць: $1",
"importunknownsource": "Невядомы тып крыніцы імпарту",
+ "importnoprefix": "Не пададзены прэфікс інтэрвікі",
"importcantopen": "Немагчыма адкрыць файл імпарту",
"importbadinterwiki": "Няслушная спасылка на іншую моўную вэрсію",
"importsuccess": "Імпартаваньне скончанае!",
"botpasswords-label-delete": "Изтриване",
"botpasswords-label-resetpassword": "Възстановяване на парола",
"botpasswords-label-grants": "Приложими разрешения:",
+ "botpasswords-label-grants-column": "Дадено",
"botpasswords-bad-appid": "Името на бота „$1“ не е валидно.",
"botpasswords-insert-failed": "Неуспешно добавяне на име на бота „$1“. Дали не е добавяно вече?",
"botpasswords-update-failed": "Неуспешно подновяване на името на бота „$1“. Дали не е изтрито?",
"rcfilters-restore-default-filters": "Възстановяване на филтрите по подразбиране",
"rcfilters-clear-all-filters": "Изчистване на всички филтри",
"rcfilters-show-new-changes": "Преглед на най-новите промени",
- "rcfilters-search-placeholder": "Филтриране на последните промени (изберете или започнете да въвеждате)",
+ "rcfilters-search-placeholder": "Филтриране на промените (използвайте менюто или търсете по име на филтър)",
"rcfilters-invalid-filter": "Невалиден филтър",
"rcfilters-empty-filter": "Няма активни филтри. Показани са всички редакции.",
"rcfilters-filterlist-title": "Филтри",
"rcfilters-filterlist-whatsthis": "Как работи това?",
- "rcfilters-filterlist-feedbacklink": "Ð\9eÑ\81Ñ\82авеÑ\82е коменÑ\82аÑ\80 за новиÑ\82е (беÑ\82а) Ñ\84илÑ\82Ñ\80и",
+ "rcfilters-filterlist-feedbacklink": "СподелеÑ\82е какво миÑ\81лиÑ\82е за Ñ\82ези (нови) инÑ\81Ñ\82Ñ\80Ñ\83менÑ\82и за Ñ\84илÑ\82Ñ\80иÑ\80ане",
"rcfilters-highlightbutton-title": "Отбелязване на резултатите",
"rcfilters-highlightmenu-title": "Изберете цвят",
"rcfilters-highlightmenu-help": "Изберете цвят за отбелязване на свойството",
"rcfilters-filter-editsbyself-description": "Ваши редакции.",
"rcfilters-filter-editsbyother-label": "Чужди редакции",
"rcfilters-filter-editsbyother-description": "Всички редакции с изключение на вашите собствени.",
- "rcfilters-filtergroup-userExpLevel": "Ð\9dиво на опиÑ\82а (Ñ\81амо за Ñ\80егиÑ\81Ñ\82Ñ\80иÑ\80ани поÑ\82Ñ\80ебиÑ\82ели)",
+ "rcfilters-filtergroup-userExpLevel": "РегиÑ\81Ñ\82Ñ\80аÑ\86иÑ\8f на поÑ\82Ñ\80ебиÑ\82елÑ\8f и опиÑ\82",
"rcfilters-filter-user-experience-level-registered-label": "Регистрирани",
"rcfilters-filter-user-experience-level-registered-description": "Влезли в системата редактори.",
"rcfilters-filter-user-experience-level-unregistered-label": "Нерегистрирани",
"rcfilters-filter-user-experience-level-unregistered-description": "Редактори, които не са влезли в системата.",
"rcfilters-filter-user-experience-level-newcomer-label": "Новодошли",
- "rcfilters-filter-user-experience-level-newcomer-description": "Ð\9fо-малко оÑ\82 10 Ñ\80едакÑ\86ии и 5 дни активност.",
+ "rcfilters-filter-user-experience-level-newcomer-description": "РегиÑ\81Ñ\82Ñ\80иÑ\80ани Ñ\80едакÑ\82оÑ\80и Ñ\81 по-малко оÑ\82 10 Ñ\80едакÑ\86ии или 4 дни активност.",
"rcfilters-filter-user-experience-level-learner-label": "Учещи се",
- "rcfilters-filter-user-experience-level-learner-description": "Ð\9fовеÑ\87е опиÑ\82 оÑ\82 â\80\9eÐ\9dоводоÑ\88лиâ\80\9c, но по-малко оÑ\82 „Опитни потребители“.",
+ "rcfilters-filter-user-experience-level-learner-description": "РегиÑ\81Ñ\82Ñ\80иÑ\80ани поÑ\82Ñ\80ебиÑ\82ели Ñ\81 опиÑ\82 оÑ\82 â\80\9eÐ\9dоводоÑ\88лиâ\80\9c до „Опитни потребители“.",
"rcfilters-filter-user-experience-level-experienced-label": "Опитни потребители",
- "rcfilters-filter-user-experience-level-experienced-description": "Ð\9fовеÑ\87е оÑ\82 30 дни акÑ\82ивноÑ\81Ñ\82 и 500 Ñ\80едакÑ\86ии.",
+ "rcfilters-filter-user-experience-level-experienced-description": "РегиÑ\81Ñ\82Ñ\80иÑ\80ани Ñ\80едакÑ\82оÑ\80и Ñ\81 повеÑ\87е оÑ\82 500 Ñ\80едакÑ\86ии и 30 дни акÑ\82ивноÑ\81Ñ\82.",
"rcfilters-filtergroup-automated": "Автоматизирани редакции",
"rcfilters-filter-bots-label": "Бот",
"rcfilters-filter-bots-description": "Редакции, направени с помощта на автоматизирани инструменти.",
"rcfilters-filter-logactions-description": "Административни действия, създавания на сметки, изтривания на страници, качвания...",
"rcfilters-filtergroup-lastRevision": "Текущи версии",
"rcfilters-filter-lastrevision-label": "Текуща версия",
- "rcfilters-filter-lastrevision-description": "Ð\9fоследната промяна на страница.",
+ "rcfilters-filter-lastrevision-description": "Само последната промяна на страница.",
"rcfilters-filter-previousrevision-label": "Не последната версия",
"rcfilters-filter-previousrevision-description": "Всички редакции, които не са „последната версия“.",
"rcfilters-filter-excluded": "Изключени",
"backend-fail-create": "Файлът „$1“ не може да бъде съхранен.",
"backend-fail-maxsize": "Файлът „$1“ не може да бъде съхранен, тъй като размерът му надвишава {{PLURAL:$2|един байт|$2 байт}}.",
"backend-fail-connect": "Не е възможно свързването към бекенда за съхранение „$1“.",
- "backend-fail-internal": "Възникна неизвестна грешка в бекенда за съхранение „1“.",
+ "backend-fail-internal": "Възникна неизвестна грешка в бекенда за съхранение „$1“.",
"zip-file-open-error": "Възникна грешка при отваряне на файла за проверка на ZIP.",
"zip-wrong-format": "Указаният файл не е ZIP файл.",
"zip-bad": "Файлът е повреден или е нечетим ZIP файл.\nСигурността му не може да бъде проверена.",
"delete_and_move_text": "Целевата страница „[[:$1]]“ вече съществува.\nИскате ли да я изтриете, за да освободите място за преместването?",
"delete_and_move_confirm": "Да, искам да изтрия тази страница",
"delete_and_move_reason": "Изтрита, за да се освободи място за преместване от „[[$1]]“",
- "selfmove": "СÑ\82Ñ\80аниÑ\86аÑ\82а не може да бÑ\8aде пÑ\80емеÑ\81Ñ\82ена, Ñ\82Ñ\8aй каÑ\82о Ñ\86елевоÑ\82о име Ñ\81Ñ\8aвпада Ñ\81 пÑ\8aÑ\80вонаÑ\87алноÑ\82о Ñ\9d заглавие.",
+ "selfmove": "Ð\97аглавиеÑ\82о Ñ\81Ñ\8aвпада;\nÑ\81Ñ\82Ñ\80аниÑ\86аÑ\82а не може да бÑ\8aде пÑ\80емеÑ\81Ñ\82ена вÑ\8aÑ\80Ñ\85Ñ\83 Ñ\81амаÑ\82а неÑ\8f.",
"immobile-source-namespace": "Не могат да се местят страници в именно пространство „$1“",
"immobile-target-namespace": "Не могат да се местят страници в именно пространство „$1“.",
"immobile-target-namespace-iw": "Страницата не може да бъде преместена под заглавие, оформено като междууики препратка.",
"autosumm-blank": "Премахване на цялото съдържание на страницата",
"autosumm-replace": "Заместване на съдържанието на страницата с „$1“",
"autoredircomment": "Пренасочване към [[$1]]",
+ "autosumm-changed-redirect-target": "Промяна на целта на пренасочване от [[$1]] на [[$2]]",
"autosumm-new": "Нова страница: „$1“",
"autosumm-newblank": "Създаване на празна страница",
"lag-warn-normal": "Промените от {{PLURAL:$1|последната $1 секунда|последните $1 секунди}} вероятно не са показани в списъка.",
"tag-mw-replace-description": "Bearbeitungen, die mehr als 90 % des Inhalts einer Seite entfernen.",
"tag-mw-rollback": "Zurücksetzung",
"tag-mw-rollback-description": "Bearbeitungen, die frühere Bearbeitungen mithilfe des Zurücksetzen-Links rückgängig machen.",
+ "tag-mw-undo": "Rückgängigmachung",
+ "tag-mw-undo-description": "Bearbeitungen, die frühere Versionen mit dem Link „Rückgängig machen“ zurücksetzen",
"tags-title": "Markierungen",
"tags-intro": "Diese Seite zeigt alle Markierungen, die für Bearbeitungen verwendet wurden, sowie deren Bedeutung. \n\nBei entsprechender Einstellung können die Missbrauchfilter beliebige Markierungen in die Versionsgeschichte setzen. Man kann die Versionsgeschichte dann nach den Markierungen filtern.",
"tags-tag": "Markierungsname",
"Luisangelrg",
"Pierpao",
"Ohlila",
- "KATRINE1992"
+ "KATRINE1992",
+ "Athena in Wonderland"
]
},
"tog-underline": "Subrayar los enlaces:",
"tag-mw-replace-description": "Ediciones que eliminan más del 90 % del contenido de una página",
"tag-mw-rollback": "Reversión",
"tag-mw-rollback-description": "Ediciones que deshacen modificaciones previas usando la herramienta de reversor",
+ "tag-mw-undo": "Deshacer",
"tags-title": "Etiquetas",
"tags-intro": "Esta página lista las etiquetas con las que el software puede marcar una edición y su significado.",
"tags-tag": "Nombre de etiqueta",
"timezoneregion-indian": "India ookean",
"timezoneregion-pacific": "Vaikne ookean",
"allowemail": "Luba teistel kasutajatel mulle e-kirju saata",
+ "email-allow-new-users-label": "Luba e-kirjad tuliuutelt kasutajatelt",
"email-blacklist-label": "Keela neil kasutajatel mulle e-kirju saata:",
"prefs-searchoptions": "Otsimine",
"prefs-namespaces": "Nimeruumid",
"right-siteadmin": "Panna lukku ja lukust lahti teha andmebaasi",
"right-override-export-depth": "Eksportida lehekülgi, kaasates viidatud leheküljed kuni viienda tasemeni",
"right-sendemail": "Saata teistele kasutajatele e-kirju",
+ "right-sendemail-new-users": "Saata e-kirju logitud toiminguteta kasutajatele",
"right-managechangetags": "Koostada ja (in)aktiveerida [[Special:Tags|märgiseid]]",
"right-applychangetags": "Rakendada [[Special:Tags|märgiseid]] enda muudatuste suhtes",
"right-changetags": "Lisada ja eemaldada käsitsi rakendatavaid [[Special:Tags|märgiseid]] üksikute redaktsioonide ja logisissekannete juures",
"rcfilters-preference-label": "Peida viimaste muudatuste täiustatud versioon",
"rcfilters-preference-help": "Pöörab tagasi 2017. aastast alates tehtud muudatused kujunduses ja lisatud tööriistad.",
"rcfilters-filter-showlinkedfrom-label": "Näita muudatusi lehekülgedel, millele viidatakse leheküljelt:",
- "rcfilters-filter-showlinkedfrom-option-label": "Näita muudatusi lehekülgedel, millele viidatakse <strong>leheküljelt</strong>",
- "rcfilters-filter-showlinkedto-label": "Näita muudatusi lehekülgedel, millel viidatakse leheküljele:",
- "rcfilters-filter-showlinkedto-option-label": "Näita muudatusi lehekülgedel, mis viitavad <strong>leheküljele</strong>",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Leheküljed, millele viidatakse</strong> valitud leheküljel",
+ "rcfilters-filter-showlinkedto-label": "Näita muudatusi lehekülgedel, millel viidatakse leheküljele",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>Leheküljed, mis viitavad</strong> valitud leheküljele",
"rcfilters-target-page-placeholder": "Sisesta lehekülje pealkiri",
"rcnotefrom": "Allpool on toodud {{PLURAL:$5|muudatus|muudatused}} alates: <strong>$3, kell $4</strong> (näidatakse kuni <strong>$1</strong> muudatust)",
"rclistfromreset": "Lähtesta kuupäeva valik",
"tag-mw-replace-description": "Muudatused, millega asendatakse lehekülje sisust enam kui 90%",
"tag-mw-rollback": "Tühistamine",
"tag-mw-rollback-description": "Muudatused, millega eelmised muudatused pööratakse tagasi, kasutades tühistuslinki",
+ "tag-mw-undo": "Eemaldamine",
+ "tag-mw-undo-description": "Muudatused, millega eelmised muudatused pööratakse tagasi, kasutades eemaldamislinki",
"tags-title": "Märgised",
"tags-intro": "See lehekülg loetleb märgised, millega tarkvara võib muudatused märgistada, ja nende kirjeldused.",
"tags-tag": "Märgise nimi",
"rcfilters-advancedfilters": "Filtres avancés",
"rcfilters-limit-title": "Résultats à afficher",
"rcfilters-limit-and-date-label": "{{PLURAL:$1|modification|$1 modifications}}, $2",
- "rcfilters-date-popup-title": "Periode de temps pour chercher",
+ "rcfilters-date-popup-title": "Période de temps à rechercher",
"rcfilters-days-title": "Derniers jours",
"rcfilters-hours-title": "Dernières heures",
"rcfilters-days-show-days": "$1 {{PLURAL:$1|jour|jours}}",
"imported-log-entries": "$1 {{PLURAL:$1|entrée|entrées}} du journal {{PLURAL:$1|importée|importées}}.",
"importfailed": "Échec de l'importation : <nowiki>$1</nowiki>",
"importunknownsource": "Type inconnu de la source à importer",
- "importnoprefix": "Aucun prefixe de interwiki n'a ete fourni",
+ "importnoprefix": "Aucun prefixe interwiki n’a été fourni",
"importcantopen": "Impossible d'ouvrir le fichier à importer",
"importbadinterwiki": "Mauvais lien inter-wiki",
"importsuccess": "L'importation a réussi !",
"tag-mw-contentmodelchange": "modification du modèle de contenu",
"tag-mw-contentmodelchange-description": "Modifications qui [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel changent le modèle de contenu] d'une page",
"tag-mw-new-redirect": "Nouvelle redirection",
- "tag-mw-new-redirect-description": "Editions qui vont creer une nouvelle redirection ou modifier una page vers une redirection",
+ "tag-mw-new-redirect-description": "Modifications qui créent une nouvelle redirection ou transforment une page en redirection",
"tag-mw-removed-redirect": "Redirection supprimée",
- "tag-mw-removed-redirect-description": "Les editions qui vont changer la redirection courante a une non redirection",
- "tag-mw-changed-redirect-target": "La destination de redirection a ete modifiee",
+ "tag-mw-removed-redirect-description": "Modifications qui remplacent une redirection existante par une page sans redirection",
+ "tag-mw-changed-redirect-target": "La destination de redirection a été modifiée",
"tag-mw-changed-redirect-target-description": "Modifications qui modifient la cible d’une redirection",
"tag-mw-blank": "Blanchiment",
"tag-mw-blank-description": "Modifications qui suppriment le contenu des pages",
"Banjo",
"Josep Maria Roca Peña",
"Luan",
- "Hamilton Abreu"
+ "Hamilton Abreu",
+ "Athena in Wonderland"
]
},
"tog-underline": "Subliñar as ligazóns:",
"rcfilters-preference-label": "Ocultar a versión mellorada de cambios recentes",
"rcfilters-preference-help": "Reverte o redeseño da interface de 2017 e tódalas ferramentas engadidas dende entón.",
"rcfilters-filter-showlinkedfrom-label": "Amosar os cambios en páxinas ligadas desde",
- "rcfilters-filter-showlinkedfrom-option-label": "Amosar os cambios en páxinas ligadas <strong>DESDE</strong> unha páxina",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Páxinas ligadas desde</strong> a páxina seleccionada",
"rcfilters-filter-showlinkedto-label": "Amosar os cambios en páxinas que ligan con",
- "rcfilters-filter-showlinkedto-option-label": "Amosar os cambios en páxinas que ligan <strong>CON</strong> unha páxina",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>Páxinas que ligan</strong> para a páxina seleccionada",
"rcfilters-target-page-placeholder": "Insire un nome de páxina",
"rcnotefrom": "A continuación {{PLURAL:$5|móstrase o cambio feito|móstranse os cambios feitos}} desde o <strong>$3</strong> ás <strong>$4</strong> (móstranse <strong>$1</strong> como máximo).",
"rclistfromreset": "Reinicializar a selección da data",
"tag-mw-replace-description": "Edicións que eliminan máis do 90% do contido dunha páxina",
"tag-mw-rollback": "Desfacer",
"tag-mw-rollback-description": "Edicións que desfán modificacións previas usando a ligazón de desfacer",
+ "tag-mw-undo": "Desfacer",
"tags-title": "Etiquetas",
"tags-intro": "Esta páxina lista as etiquetas coas que o software pode marcar unha edición, e mailos seus significados.",
"tags-tag": "Nome da etiqueta",
"tag-mw-replace-description": "עריכות שמסירות יותר מ־90% מהתוכן של דף",
"tag-mw-rollback": "שחזור",
"tag-mw-rollback-description": "עריכות שמשחזרות עריכות קודמות בעזרת קישור השחזור",
+ "tag-mw-undo": "ביטול",
+ "tag-mw-undo-description": "עריכות שמבטלות עריכות קודמות בעזרת קישור הביטול",
"tags-title": "תגיות",
"tags-intro": "דף זה מכיל רשימה של תגיות שהתוכנה יכולה לסמן איתן עריכה, ומשמעויותיהן.",
"tags-tag": "שם התגית",
"enotif_impersonal_salutation": "{{SITENAME}} सदस्य",
"enotif_subject_deleted": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने हटा दिया है",
"enotif_subject_created": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने बना दिया है",
- "enotif_subject_moved": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने स्थानांतरित कर दिया है",
+ "enotif_subject_moved": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|चले गए}} $2 द्वारा चले जा चुका है",
"enotif_subject_restored": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने पुनर्स्थापित कर दिया है",
"enotif_subject_changed": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने परिवर्तित किया है",
"enotif_body_intro_deleted": "{{SITENAME}} पृष्ठ $1 को {{GENDER:$2|$2}} ने $PAGEEDITDATE को हटा दिया है, देखें $3।",
"tag-mw-replace-description": "संपादन जिसने 90% से अधिक पृष्ट की सामग्री को हटा दिया",
"tag-mw-rollback": "पीछे हटना",
"tag-mw-rollback-description": "संपादन जो रोलबैक लिंक का उपयोग करके पिछला संपादन वापस रोल करता है",
+ "tag-mw-undo": "किए हुए कार्य को पूर्वत करना",
+ "tag-mw-undo-description": "संपादन जो पिछले लिंक का उपयोग करके पिछले संपादन को पूर्वत करता है",
"tags-title": "चिप्पियाँ",
"tags-intro": "यह पृष्ठ अर्थ सहित वह चिप्पियाँ दर्शाता है जिनका कोई तंत्रांश किसी संपादन पर निशान लगाने के लिए इस्तेमाल कर सकता है।",
"tags-tag": "चिप्पी का नाम",
"permissionserrorstext-withaction": "Jūs neturite leidimo $2 dėl {{PLURAL:$1|šios priežasties|šių priežasčių}}:",
"contentmodelediterror": "Jūs negalite redaguoti šios versijos, nes jos turinio modelis yra <code>$1</code>, kuris skiriasi nuo dabartinio puslapio turinio modelio, kuris yra <code>$2</code>.",
"recreate-moveddeleted-warn": "'''Dėmesio: Jūs atkuriate puslapį, kuris anksčiau buvo ištrintas.'''\n\nTurėtumėte nuspręsti, ar reikėtų toliau redaguoti šį puslapį.\nJūsų patogumui čia pateikiamas šio puslapio šalinimų ir perkėlimų sąrašas:",
- "moveddeleted-notice": "Šis puslapis buvo ištrintas.\nŽemiau pateikiamas puslapio šalinimų ir pervadinimų sąrašas.",
+ "moveddeleted-notice": "Šis puslapis buvo ištrintas.\nŽemiau pateikiamas puslapio šalinimų, apsaugojimų, ir pervadinimų sąrašas.",
"moveddeleted-notice-recent": "Atsiprašome, šis puslapis neseniai buvo ištrintas (per pastarąsias 24 valandas). Žemiau pateikiama detali puslapio ištrynimo ir perkėlimo istorija.",
"log-fulllog": "Rodyti visą istoriją",
"edit-hook-aborted": "Keitimas nutrauktas užlūžimo.\nTam nėra paaiškinimo.",
"unwatchthispage": "Nustoti stebėti",
"notanarticle": "Ne turinio puslapis",
"notvisiblerev": "Versija buvo ištrinta",
- "watchlist-details": "Stebima {{PLURAL:$1|$1 puslapis|$1 puslapiai|$1 puslapių}}, neskaičiuojant aptarimų puslapių.",
+ "watchlist-details": "Stebima {{PLURAL:$1|$1 puslapis|$1 puslapiai|$1 puslapių}}, (įskaičiuojant aptarimų puslapius).",
"wlheader-enotif": "El. pašto pranešimai yra įjungti.",
"wlheader-showupdated": "Puslapiai pakeisti nuo tada, kai paskutinį kartą apsilankėte juose, yra '''paryškinti'''.",
"wlnote": "{{PLURAL:$1|Rodomas '''$1''' paskutinis pakeitimas, atliktas|Rodomi '''$1''' paskutiniai pakeitimai, atlikti|Rodoma '''$1''' paskutinių pakeitimų, atliktų}} per '''$2''' {{PLURAL:$2|paskutinę valandą|paskutines valandas|paskutinių valandų}}, nuo $3 $4.",
"rcfilters-preference-label": "Ocultar a versão melhorada das mudanças recentes",
"rcfilters-preference-help": "Reverte o redesenho da interface de 2017 e todas as ferramentas adicionadas na altura e desde então.",
"rcfilters-filter-showlinkedfrom-label": "Mostrar mudanças de páginas para as quais esta página contém hiperligações",
- "rcfilters-filter-showlinkedfrom-option-label": "<strong>Páginas que contêm hiperligações</strong> da página selecionada",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Páginas para as quais</strong> a página selecionada contém hiperligações",
"rcfilters-filter-showlinkedto-label": "Mostrar mudanças nas páginas que contêm hiperligações para",
"rcfilters-filter-showlinkedto-option-label": "<strong>Páginas que contêm hiperligações</strong> para a página selecionada",
"rcfilters-target-page-placeholder": "Introduzir o nome de uma página",
"tag-mw-replace-description": "Edições que removem mais de 90% do conteúdo de uma página",
"tag-mw-rollback": "Reversão",
"tag-mw-rollback-description": "Edições que revertem edições anteriores usando a hiperligação desfazer",
+ "tag-mw-undo": "Desfazer",
"tags-title": "Etiquetas de modificação válidas",
"tags-intro": "Esta página lista as etiquetas com que o software poderá marcar uma edição, e o seu significado.",
"tags-tag": "Nome da etiqueta",
"diff-form-oldid": "Identificador de revisão antigo (opcional)",
"diff-form-revid": "Identificador de revisão da diferença",
"diff-form-submit": "Mostrar diferenças",
- "permanentlink": "Link permanente",
+ "permanentlink": "Hiperligação permanente",
"permanentlink-revid": "Identificador de revisão",
"permanentlink-submit": "Ir para a revisão",
"dberr-problems": "Desculpe! Este site está com dificuldades técnicas.",
"right-siteadmin": "блокировка и разблокировка базы данных",
"right-override-export-depth": "экспортирование страниц, включая связанные страницы с глубиной до 5",
"right-sendemail": "отправка электронной почты другим участникам",
+ "right-sendemail-new-users": "отправка электронной почты участникам без записей журналов",
"right-managechangetags": "создание и (де)активация [[Special:Tags|меток]]",
"right-applychangetags": "применение [[Special:Tags|меток]] вместе со своими правками",
"right-changetags": "добавление и удаление произвольных [[Special:Tags|меток]] на отдельных правках и записях в журнале",
"recentchanges-noresult": "Изменений в указанный период, соответствующих указанным условиям, нет.",
"recentchanges-timeout": "Время ожидания этого поиска истекло. Вы можете попробовать задать другие параметры поиска.",
"recentchanges-network": "Из-за технической ошибки результаты не могут быть загружены. Попробуйте обновить страницу.",
+ "recentchanges-notargetpage": "Введите название страницы выше, чтобы увидеть правки, связанные с этой страницей.",
"recentchanges-feed-description": "Отслеживание последних изменений в вики.",
"recentchanges-label-newpage": "Этой правкой была создана новая страница",
"recentchanges-label-minor": "Это незначительное изменение",
"rcfilters-watchlist-showupdated": "Изменения страниц, которые вы не посещали с того момента, как они изменились, выделены <strong>жирным</strong> и отмечены полным маркером.",
"rcfilters-preference-label": "Скрыть улучшенную версию Последних изменений",
"rcfilters-preference-help": "Откатывает редизайн интерфейса 2017 года и все инструменты, добавленные с тех пор.",
+ "rcfilters-filter-showlinkedfrom-label": "Показать правки на ссылаемых страницах",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Страницы, на которые ссылается</strong> выбранная",
+ "rcfilters-filter-showlinkedto-label": "Показать правки на ссылающихся страницах",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>Страницы, ссылающиеся</strong> на выбранную",
"rcfilters-target-page-placeholder": "Введите имя страницы",
"rcnotefrom": "Ниже {{PLURAL:$5|указано изменение|перечислены изменения}} с <strong>$3, $4</strong> (показано не более <strong>$1</strong>).",
"rclistfromreset": "Сбросить выбор даты",
"uploadstash-bad-path-unrecognized-thumb-name": "Нераспознанное имя миниатюры.",
"uploadstash-bad-path-no-handler": "Не найден обработчик для mime-типа $1 файла $2.",
"uploadstash-bad-path-bad-format": "Ключ «$1» — в неподходящем формате.",
+ "uploadstash-file-not-found": "Ключ «$1» не найден во временном хранилище.",
"uploadstash-file-not-found-no-thumb": "Не удалось получить миниатюру.",
+ "uploadstash-file-not-found-no-local-path": "Не найдено локального пути к отмасштабированному изображению.",
"uploadstash-file-not-found-no-object": "Не удалось создать объект локального файла для миниатюры.",
"uploadstash-file-not-found-no-remote-thumb": "Извлечение эскиза не удалось: $1\nURL = $2",
"uploadstash-file-not-found-missing-content-type": "Отсутствует заголовок content-type.",
+ "uploadstash-file-not-found-not-exists": "Путь не найден или файл непонятен.",
"uploadstash-file-too-large": "Невозможно обработать файл размером более $1 байт.",
+ "uploadstash-not-logged-in": "Нет авторизованных участников, файлы должны принадлежать участникам.",
"uploadstash-wrong-owner": "Этот файл ($1) не принадлежит текущему участнику.",
+ "uploadstash-no-such-key": "Нет ключа ($1), удаление невозможно.",
"uploadstash-no-extension": "Пустое расширение.",
"uploadstash-zero-length": "Файл нулевой длины.",
"invalid-chunk-offset": "Недопустимое смещение фрагмента",
"upload-disallowed-here": "Вы не можете перезаписать этот файл.",
"filerevert": "Возврат к старой версии $1",
"filerevert-legend": "Возвратить версию файла",
- "filerevert-intro": "<span class=\"plainlinks\">Вы возвращаете '''[[Media:$1|$1]]''' к [$4 версии от $3, $2].</span>",
+ "filerevert-intro": "Вы возвращаете <strong>[[Media:$1|$1]]</strong> к [$4 версии от $3, $2].",
"filerevert-comment": "Причина:",
"filerevert-defaultcomment": "Возврат к версии от $2, $1 ($3)",
"filerevert-submit": "Возвратить",
"filedelete": "$1 — удаление",
"filedelete-legend": "Удалить файл",
"filedelete-intro": "Вы собираетесь удалить файл '''[[Media:$1|$1]]''' со всей его историей.",
- "filedelete-intro-old": "<span class=\"plainlinks\">Вы удаляете версию '''[[Media:$1|$1]]''' от [$4 $3, $2].</span>",
+ "filedelete-intro-old": "Вы удаляете версию <strong>[[Media:$1|$1]]</strong> от [$4 $3, $2].",
"filedelete-comment": "Причина:",
"filedelete-submit": "Удалить",
"filedelete-success": "'''$1''' был удалён.",
"import-mapping-subpage": "Импортировать как подстраницы следующей страницы:",
"import-upload-filename": "Имя файла:",
"import-upload-username-prefix": "Префиксы интервики:",
+ "import-assign-known-users": "Связать правки с локальными участниками, когда участники с такими именами существуют.",
"import-comment": "Примечание:",
"importtext": "Пожалуйста, экспортируйте страницу из исходной вики, используя [[Special:Export|соответствующий инструмент]]. Сохраните файл на диск, а затем загрузите его сюда.",
"importstart": "Импортирование страниц…",
"imported-log-entries": "{{PLURAL:$1|Импортирована $1 запись|Импортированы $1 записи|Импортировано $1 записей}} журнала.",
"importfailed": "Не удалось импортировать: $1",
"importunknownsource": "Неизвестный тип импортируемой страницы",
+ "importnoprefix": "Не указан префикс интервики",
"importcantopen": "Невозможно открыть импортируемый файл",
"importbadinterwiki": "Неправильная интервики-ссылка",
"importsuccess": "Импортирование выполнено!",
"cannotdelete": "$1 نالي صفحو يا فائيل ڊهي نہ سگھيو. ٿي سگھي ٿو تہ ڪنهن ان کي اڳ ۾ ئي ڊاهي ڇڏيو هجي.",
"cannotdelete-title": "$1 نالي صفحي کي ڊاهي نہ ٿا سگھون.",
"badtitle": "خراب عنوان",
- "badtitletext": "صفحي جو گھربل عنوان ڪار ڪونهي، يا خالي آهي، يا وري غيردرست طريقي سان ڳنڍيل بينالزباني يا بينالوڪي عنوان آهي. \nان ۾ هڪ يا هڪ کان وڌيڪ اهڙا اکر موجود آهن، جيڪي عنوان ۾ استعمال ڪري نہ ٿا سگھجن.",
- "title-invalid-utf8": "صفحي جي ڄاڻايل عنوان ۾ ناقابل ڪار يُو ٽِي ايف اکر شامل آهن.",
- "title-invalid-interwiki": "ڄاڻايل عنوان ۾ اهڙو بينالوڪِي ڳنڍڻو شامل آهي، جيڪو عنوانن ۾ استعمال ڪري نہ ٿو سگھجي.",
- "title-invalid-characters": "صفحي جي ڄاڻايل عنوان ۾ ناقابل ڪار اکر شامل آهن: $1",
- "title-invalid-leading-colon": "صفحي جي ڄاڻايل عنوان جي ابتدا ۾ ناقابل ڪار ڪالن شامل آهي.",
+ "badtitletext": "صفحي جو گھربل عنوان ڪار ڪونهي، يا خالي آهي، يا وري غيردرست طريقي سان ڳنڍيل بينالزباني يا بينالوڪي عنوان آهي. \nان ۾ هڪ يا هڪ کان وڌيڪ اهڙا اکر موجود آهن، جيڪي عنوان ۾ استعمال ڪري نٿا سگھجن.",
+ "title-invalid-utf8": "صفحي جي ڄاڻايل عنوان ۾ ناقابلِڪار يُو ٽِيئيف-8 ترتيب شامل آھي.",
+ "title-invalid-interwiki": "ڄاڻايل عنوان ۾ اهڙو بينالوڪِي ڳنڍڻو شامل آهي، جيڪو عنوانن ۾ استعمال ڪري نٿو سگھجي.",
+ "title-invalid-characters": "صفحي جي ڄاڻايل عنوان ۾ ناقابلِڪار اکر شامل آهن: \"$1\".",
+ "title-invalid-leading-colon": "صفحي جي ڄاڻايل عنوان جي ابتدا ۾ ناقابلِڪار ڪالن شامل آهي.",
"viewsource": "ڪوڊ ڏسو",
"viewsource-title": "$1 جو ڪوڊ ڏسو",
- "protectedpagetext": "هيءُ صفحو ترميمن کان تحفظيل آهي.",
- "viewsourcetext": "توهان هن صفحي جو ڪوڊ ڏسي ۽ نقل ڪري سگھو ٿا:",
- "namespaceprotected": "توهان کي نانءُ پولار '''$1''' جا صفحا سنوارڻ جا اختيار ناهن.",
+ "protectedpagetext": "هيءُ صفحو ترميمن ۽ ٻين عملن کان بچائڻ لاءِ تحفظيل آهي.",
+ "viewsourcetext": "توهان هن صفحي جو ڪوڊ ڏسي ۽ نقل ڪري سگھو ٿا.",
+ "namespaceprotected": "توهان کي نانءُپولار <strong>$1</strong> جا صفحا سنوارڻ جا اختيار ناهن.",
"mycustomcssprotected": "توهان کي هيءُ CSS صفحو سنوارڻ جي اجازت نہ آهي.",
"mycustomjsprotected": "توهان کي هيءُ جاوا اسڪرپٽ صفحو سنوارڻ جي اجازت حاصل ڪانهي.",
"myprivateinfoprotected": "توهان کي پنهنجي ذاتي معلومات سنوارڻ جي اجازت حاصل نہ آهي.",
"virus-unknownscanner": "اڻڄاتل نِس وائرس:",
"cannotlogoutnow-title": "ھاڻي خارج نٿو ٿي سگھجي",
"cannotlogoutnow-text": "$1 استعمال ڪرڻ دوران خارج ٿيڻ ممڪن نہ آھي.",
- "welcomeuser": "ڀلي ڪري آيا، $1!",
+ "welcomeuser": "ڀليڪار، $1!",
"yourname": "واپرائيندڙ-نانءُ:",
"userlogin-yourname": "واپرائيندڙ-نانءُ",
"userlogin-yourname-ph": "پنھنجو يوزرنانءُ ڄاڻايو",
"nosuchusershort": "\"$1\" نالي ڪو بہ واپرائيندڙ ناهي.\nپنھنجي هِجي جي پڪ ڪندا.",
"nouserspecified": "توهان کي ڪو واپرائيندڙ-نانءُ ڄاڻائڻو پوندو.",
"login-userblocked": "هيءُ واپرائيندڙ بندشيل آهي. داخل ٿيڻ جي اجازت نٿي ڏجي.",
- "wrongpassword": "ڏنل ڳجھولفظ غير درست آهي. مھرباني ڪري ٻيھر ڪوشش ڪندا.",
+ "wrongpassword": "ڏنل واپرائيندڙ-نانءُ يا ڳجھولفظ غير درست آهي.\nمھرباني ڪري ٻيھر ڪوشش ڪندا.",
"wrongpasswordempty": "ڏنل ڳجھولفظ خالي هو.\nمهرباني ڪري وري ڪوشش ڪندا.",
"passwordtooshort": "ڳجھولفظ گھٽ ۾ گھٽ {{PLURAL:$1|1 اکر|$1 اکرَن}} تي ٻڌل هوڻ گھرجي.",
"passwordtoolong": "ڳجھولفظ {{PLURAL:$1|1 اکر|$1 اکرن}} کان وڏو نٿو ٿي سگھي.",
"login-abort-generic": "توهان جو داخل ٿيڻ ناڪام ويو - بند ڪيل",
"login-migrated-generic": "توهان جو کاتو لڏي چڪو آهي، ۽ هن وڪيءَ تي توهان جو واپرائيندڙ-نانءُ هاڻي وجود نٿو رکي.",
"loginlanguagelabel": "ٻولي: $1",
- "createacct-another-realname-tip": "اصل نالو ڄاڻائڻ اختياري آهي. جيڪڏهن توهان اصل نالو ڄاڻايو ٿا، تہ اهو توهان کي توهان جي ڪم جي مڃتا ڏيڻ لاءِ ڪم آندو ويندو.",
+ "createacct-another-realname-tip": "اصل نالو ڄاڻائڻ اختياري آھي.\nجيڪڏھن توھان اھو ڄاڻائڻ چونڊيو ٿا، تہ اھو واپرائيندڙ کي انھن جي ڪم جي مڃتا ڏيڻ لاءِ ڪم آندو ويندو.",
"pt-login": "داخل ٿيو",
"pt-login-button": "داخل ٿيو",
"pt-login-continue-button": "داخل ٿيڻ جاري رکو",
"pt-createaccount": "کاتو کوليو",
"pt-userlogout": "خارج ٿيو",
- "php-mail-error-unknown": "پي ايڇ پي جي ڪاڄ اندر اڻڄاتل چُڪَ.",
+ "php-mail-error-unknown": "پي ايڇ پي جي ڪاڄ() اندر اڻڄاتل چُڪَ.",
"user-mail-no-addy": "برقٽپال پتو ڄاڻائڻ کان سواءِ برقٽپال اماڻڻ جي ڪوشش ڪئي وئي.",
"changepassword": "ڳجھولفظ تبديل ڪريو",
"resetpass_announce": "داخل ٿيڻ جو عمل پورو ڪرڻ لاءِ، توهان کي نئون ڳجھولفظ اختيار مقرر ڪرڻو پوندو.",
"botpasswords-label-create": "سرجيو",
"botpasswords-label-update": "تجديد",
"botpasswords-label-cancel": "رد",
- "botpasswords-label-delete": "ڊاهيو",
+ "botpasswords-label-delete": "ڊاھيو",
"botpasswords-label-resetpassword": "ڳجھولفظ ٻيھر مقرر ڪريو",
"botpasswords-label-grants-column": "منظور",
"botpasswords-bad-appid": "بوٽ نانءُ \"$1\" قابلِڪار ناھي.",
"passwordreset": "ڳجھولفظ مَٽايو",
"passwordreset-text-one": "برقٽپال ذريعي عارضي ڳجھولفظ حاصل ڪرڻ لاءِ هيءُ فارم پُر ڪريو.",
"passwordreset-disabled": "هن وڪيءَ تي ڳجھولفظ ٻيھر مقرر ڪرڻ وارو چارو غير فعال بڻايو ويو آهي.",
- "passwordreset-emaildisabled": "هن وڪيءَ تي برقٽپال واريون خصوصيتون غير فعال بڻايون ويون آهن.",
+ "passwordreset-emaildisabled": "ھن وڪيءَ تي برقٽپال واريون خصوصيتون غير فعال بڻايون ويون آهن.",
"passwordreset-username": "واپرائيندڙ-نانءُ:",
"passwordreset-domain": "ميدان:",
"passwordreset-email": "برقٽپال پتو:",
"bold_sample": "گھري لکت",
"bold_tip": "گھري لکت",
"italic_sample": "ترڇي لکت",
- "italic_tip": "ترڇي لکت",
+ "italic_tip": "ٽيڏي لکت",
"link_sample": "ڳنڍڻي جو عنوان",
"link_tip": "داخلي ڳنڍڻو",
"extlink_sample": "http://www.example.com ڳنڍڻي جو عنوان",
"showdiff": "تبديليون ڏيکاريو",
"anoneditwarning": "<strong>چتاءُ:</strong> توھان داخل ٿيل نہ آھيو. توھان جو آءِپي پتو عوامي طور ظاھر ٿيندو جي توھان ڪي ترميمون ڪريو ٿا. جيڪڏھن توھان <strong>[$1 داخل ٿيو]</strong> ٿا يا <strong>[$2 کاتو کوليو]</strong> ٿا، تہ ٻين فائدن سان گڏ توھان جون ترميمون توھان جي يوزرنانءَ سان منسوب ڪيون وينديون.",
"anonpreviewwarning": "توهان داخل ٿيل نہ آهيو. جيڪڏهن توهان صفحي ۾ تبديليون سانڍيون تہ اهڙين تبديلين ساڻ توهان جو آءِپي پتو درج ڪيو ويندو.",
- "missingcommenttext": "براءِ مھرباني هيٺ پنهنجو تاثر درج ڪندا.",
+ "missingcommenttext": "براءِ مھرباني ڪو تاثر درج ڪندا.",
"summary-preview": "تت جي پيش نگاھ:",
"subject-preview": "موضوع جي پيش نگاھ:",
"blockedtitle": "واپرائيندڙ بندشيل آهي",
"editingsection": "ترميم ھيٺ $1 (سيڪشن)",
"editingcomment": "ترميم هيٺ $1 (نئون سيڪشن)",
"editconflict": "ترميمي تڪرار: $1",
- "yourtext": "تÙ\88Ù\87اÙ\86 جÙ\88 Ù½Ù\8aڪسٽ",
+ "yourtext": "تÙ\88Ù\87اÙ\86 جÙ\88 Ù\85تÙ\86",
"storedversion": "سانڍيل مسودو",
"yourdiff": "تفاوت",
"copyrightwarning": "ياد رکندا ته {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 تحت پڌريون ڪجن ٿيون (تفصيلن لاءِ $1 ڏسندا). اوهان جي تحرير کي {{SITENAME}} جي قائدن تحت ترميمي سگهجي ٿو. جيڪڏهن اوهان نه ٿا چاهيو ته اوهان جي لکڻين کي بي رحميءَ سان ترميميو وڃي يا ورهائي عام ڪيو وڃي ته پوءِ پنهنجي لکڻي هتي جمع نه ڪرايو. پنهنجو مواد هتي جمع ڪرڻ جو مطلب هوندو ته توهان کي جمع ڪرايل مواد جي مفت فراهمي ۽ کُليل تبديليءَ تي ڪو به اعتراز ناهي.<br />\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو ته توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن مفت وسيلي تان ڪاپي ڪيو آهي.\n'''تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ کان سواءِ هتي جمع نه ڪريو.'''",
"timezoneregion-europe": "يُورپ",
"timezoneregion-indian": "سنڌي ساگر",
"timezoneregion-pacific": "ماٺو ساگر",
- "allowemail": "Ù»Ù\8aÙ\86 Ù\8aÙ\8fÙ\88زرس کاÙ\86 اÙ\8aÙ\86دÚ\99 ٽپاÙ\84 بØاÙ\84 ڪريو",
+ "allowemail": "Ù»Ù\8aÙ\86 Ù\88اپرائÙ\8aÙ\86دÚ\99Ù\86 Ú©Ù\8a Ù\85Ù\88Ù\86 Ú\8fاÙ\86Ú¾Ù\86 برÙ\82ٽپاÙ\84 ڪرڻ جÙ\8a اجازت Ú\8fيو",
"prefs-searchoptions": "ڳولا",
"prefs-namespaces": "نانءُپولار",
"default": "ڏنل",
"rcfilters-quickfilters": "سانڍيل ڇاڻيون",
"rcfilters-savedqueries-defaultlabel": "سانڍيل ڇاڻيون",
"rcfilters-restore-default-filters": "ڏنل ڇاڻيون ريسٽور ڪريو",
- "rcfilters-search-placeholder": "تازÙ\8aÙ\88Ù\86 تبدÙ\8aÙ\84Ù\8aÙ\88Ù\86 Ú\87اڻÙ\8aÙ\88 (Ú\87اÙ\86Ú¯Ù\8aÙ\88 Ù\8aا Ù\84Ú©Ú» شرÙ\88ع ڪريو)",
+ "rcfilters-search-placeholder": "تبدÙ\8aÙ\84Ù\8aÙ\88Ù\86 Ú\87اڻÙ\8aÙ\88 (Ù\85Ù\8aÙ\86Ù\8aÙ\88 استعÙ\85اÙ\84 ڪرÙ\8aÙ\88 Ù\8aا Ú\87اڻÙ\8aØ¡Ù\8e جÙ\8a Ú³Ù\88Ù\84ا ڪريو)",
"rcfilters-empty-filter": "ڪي بہ سرگرم ڇاڻيون ناھن. سڀ ڀاڱيداريون ڏيکاريل آھن.",
"rcfilters-filterlist-title": "ڇاڻيون",
"rcfilters-filterlist-whatsthis": "هي ڪيئن ڪم ڪن ٿا؟",
"tag-mw-replace-description": "Urejanja, ki odstranijo več kot 90 % vsebine strani",
"tag-mw-rollback": "Vrnitev",
"tag-mw-rollback-description": "Urejanja, ki vrnejo prejšnja urejanja s povezavo za vrnitev",
+ "tag-mw-undo": "Razveljavljeno",
+ "tag-mw-undo-description": "Urejanja, ki razveljavijo prejšnja urejanja z uporabo povezave za razveljavitev",
"tags-title": "Etikete",
"tags-intro": "Ta stran navaja etikete, s katerimi lahko programje označi urejanja, in njihov pomen.",
"tags-tag": "Ime oznake",
"nosuchusershort": "Користувача з іменем «$1» не існує.\nПеревірте правильність написання імені.",
"nouserspecified": "Ви повинні зазначити ім'я користувача.",
"login-userblocked": "Цей користувач заблокований. Вхід в систему не дозволений.",
- "wrongpassword": "Ð\92и ввели Ñ\85ибний паÑ\80олÑ\8c. СпÑ\80обÑ\83йÑ\82е Ñ\89е Ñ\80аз.",
+ "wrongpassword": "Ð\92и ввели Ñ\85ибне Ñ\96м'Ñ\8f коÑ\80иÑ\81Ñ\82Ñ\83ваÑ\87а або паÑ\80олÑ\8c. Ð\91Ñ\83дÑ\8c лаÑ\81ка, Ñ\81пÑ\80обÑ\83йÑ\82е зновÑ\83.",
"wrongpasswordempty": "Ви не ввели пароль. Будь ласка, спробуйте ще раз.",
"passwordtooshort": "Ваш пароль закороткий, він має містити принаймні $1 {{PLURAL:$1|символ|символи|символів}}.",
"passwordtoolong": "Пароль не може бути довшим ніж {{PLURAL:$1|1 символ|$1 символи|$1 символів}}.",
"botpasswords-insert-failed": "Не вдалось додати бота з іменем «$1». Можливо, він вже був доданий?",
"botpasswords-update-failed": "Не вдалось оновити бота з іменем «$1». Можливо, він був видалений?",
"botpasswords-created-title": "Пароль бота створено",
- "botpasswords-created-body": "Пароль бота з ім'ям «$1» користувача «$2» було створено.",
+ "botpasswords-created-body": "Пароль бота з ім'ям «$1» {{GENDER:$2|користувача|користувачки}} «$2» було створено.",
"botpasswords-updated-title": "Пароль бота оновлено",
- "botpasswords-updated-body": "Пароль бота з ім'ям «$1» користувача «$2» було оновлено.",
+ "botpasswords-updated-body": "Пароль бота з ім'ям «$1» {{GENDER:$2|користувача|користувачки}} «$2» було оновлено.",
"botpasswords-deleted-title": "Пароль бота видалено",
- "botpasswords-deleted-body": "Пароль бота з ім'ям «$1» користувача «$2» було видалено",
+ "botpasswords-deleted-body": "Пароль бота з ім'ям «$1» {{GENDER:$2|користувача|користувачки}} «$2» було вилучено",
"botpasswords-newpassword": "Новий пароль для входу під <strong>$1</strong> — <strong>$2</strong>. <em>Запишіть його для подальшого використання.</em> <br> (Для старих ботів, які вимагають, щоб логін був такий же, як і ім'я користувача, Ви також можете використовувати <strong>$3</strong> як ім'я користувача і <strong>$4</strong> як пароль.)",
"botpasswords-no-provider": "BotPasswordsSessionProvider не доступний.",
"botpasswords-restriction-failed": "Вхід не було здійснено через обмеження для паролю бота.",
"diff-multi-sameuser": "(Не {{PLURAL:$1|показано одну проміжну версію|показані $1 проміжні версії|показано $1 проміжних версій}} цього користувача)",
"diff-multi-otherusers": "(Не {{PLURAL:$1|показана $1 проміжна версія|показано $1 проміжні версії|показані $1 проміжних версій}} {{PLURAL:$2|ще одного користувача|$2 користувачів}})",
"diff-multi-manyusers": "({{PLURAL:$1|не показана $1 проміжна версія|не показані $1 проміжні версії|не показано $1 проміжних версій}}, зроблених більш, ніж {{PLURAL:$2|1=$1 користувачем|$2 користувачами}})",
+ "diff-paragraph-moved-tonew": "Абзац було переміщено. Натисніть щоб перестрибнути до нового розташування.",
+ "diff-paragraph-moved-toold": "Абзац було переміщено. Натисніть щоб перестрибнути до старого розташування.",
"difference-missing-revision": "{{PLURAL:$2|$2 версія|$2 версії|$2 версій}} для цього порівняння ($1) не {{PLURAL:$2|1=знайдена|знайдені}}.\n\nІмовірно, ви перейшли за застарілим посиланням на порівняння версій вилученої сторінки.\nПодробиці можна дізнатися з [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журналу вилучень].",
"searchresults": "Результати пошуку",
"searchresults-title": "Результати пошуку для «$1»",
"timezoneregion-indian": "Індійський океан",
"timezoneregion-pacific": "Тихий океан",
"allowemail": "Дозволити електронну пошту від інших користувачів",
+ "email-allow-new-users-label": "Дозволити електронні листи від новозареєстрованих користувачів.",
"email-blacklist-label": "Заборонити цим користувачам надсилати мені електронну пошту:",
"prefs-searchoptions": "Пошук",
"prefs-namespaces": "Простори назв",
"right-siteadmin": "Блокування і розблокування бази даних",
"right-override-export-depth": "експорт сторінок, включаючи пов'язані сторінки з глибиною до 5",
"right-sendemail": "надсилання електронної пошти іншим користувачам",
+ "right-sendemail-new-users": "надсилати електронні листи до користувачів без логованих дій",
"right-managechangetags": "створення та (де)активування [[Special:Tags|міток]]",
"right-applychangetags": "додавання [[Special:Tags|міток]] разом зі змінами",
"right-changetags": "додавання або вилучення будь-яких [[Special:Tags|міток]] для певних версій сторінок або записів журналів",
"recentchanges-summary": "Відстеження останніх змін на сторінках {{grammar:genitive|{{SITENAME}}}}.",
"recentchanges-noresult": "Немає змін за даний період, що відповідають цим критеріям.",
"recentchanges-timeout": "Час, відведений на цей пошук, вичерпано. Можливо, Ви захочете спробувати інші пошукові параметри.",
+ "recentchanges-network": "Через технічну помилку не вдалось завантажити результати. Будь ласка, спробуйте перезавантажити сторінку.",
+ "recentchanges-notargetpage": "Щоб побачити зміни пов'язані зі сторінкою, уведіть її назву вище.",
"recentchanges-feed-description": "Відстежувати останні зміни у вікі в цьому потоці.",
"recentchanges-label-newpage": "Цим редагуванням створена нова сторінка",
"recentchanges-label-minor": "Це незначна зміна",
"rcfilters-savedqueries-apply-and-setdefault-label": "Створити стандартний фільтр",
"rcfilters-savedqueries-cancel-label": "Скасувати",
"rcfilters-savedqueries-add-new-title": "Зберегти поточні налаштування фільтрів",
+ "rcfilters-savedqueries-already-saved": "Ці фільтри вже збережено. Змініть свої налаштування щоб створити новий Збережений фільтр.",
"rcfilters-restore-default-filters": "Відновити стандартні фільтри",
"rcfilters-clear-all-filters": "Очистити фільтри",
"rcfilters-show-new-changes": "Переглянути найновіші зміни",
- "rcfilters-search-placeholder": "Фільтруйте нові редагування (перегляньте або почніть вводити)",
+ "rcfilters-search-placeholder": "Фільтруйте редагування (використовуйте меню, або скористайтесь пошуком фільтру за назвою)",
"rcfilters-invalid-filter": "Недійсний фільтр",
"rcfilters-empty-filter": "Без фільтрів. Показано всі зміни.",
"rcfilters-filterlist-title": "Фільтри",
"rcfilters-watchlist-showupdated": "Зміни до сторінок, які Ви не відвідували з моменту здійснення змін, виділені <strong>жирним</strong>, із цілісними маркерами.",
"rcfilters-preference-label": "Приховати покращену версію Нових редагувань",
"rcfilters-preference-help": "Скасовує зміну дизайну 2017 року та всі інструменти, додані тоді й пізніше.",
+ "rcfilters-filter-showlinkedfrom-label": "Показати зміни на сторінках, на які звідси посилання",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>Сторінки, на які є посилання з</strong> обраної сторінки",
+ "rcfilters-filter-showlinkedto-label": "Показати зміни на сторінках, що посилаються сюди",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>Сторінки, що посилаються на</strong> обрану сторінку",
+ "rcfilters-target-page-placeholder": "Уведіть назву сторінки",
"rcnotefrom": "Нижче знаходяться {{PLURAL:$5|редагування}} з <strong>$3, $4</strong> (відображено до <strong>$1</strong>).",
"rclistfromreset": "Скинути вибір дати",
"rclistfrom": "Показати редагування починаючи з $3 $2.",
"recentchangeslinked-feed": "Пов'язані редгування",
"recentchangeslinked-toolbox": "Пов'язані редагування",
"recentchangeslinked-title": "Пов'язані редагування для «$1»",
- "recentchangeslinked-summary": "Це Ñ\81пиÑ\81ок неÑ\89одавнÑ\96Ñ\85 змÑ\96н на Ñ\81Ñ\82оÑ\80Ñ\96нкаÑ\85, на Ñ\8fкÑ\96 поÑ\81илаÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f зазнаÑ\87ена Ñ\81Ñ\82оÑ\80Ñ\96нка (або на Ñ\81Ñ\82оÑ\80Ñ\96нкаÑ\85, Ñ\89о мÑ\96Ñ\81Ñ\82Ñ\8fÑ\82Ñ\8cÑ\81Ñ\8f в Ñ\86Ñ\96й каÑ\82егоÑ\80Ñ\96Ñ\97).\nСÑ\82оÑ\80Ñ\96нки з [[Special:Watchlist|Ð\92аÑ\88ого Ñ\81пиÑ\81кÑ\83 Ñ\81поÑ\81Ñ\82еÑ\80еженнÑ\8f]] видÑ\96лено '''жиÑ\80ним Ñ\88Ñ\80иÑ\84Ñ\82ом'''.",
+ "recentchangeslinked-summary": "УведÑ\96Ñ\82Ñ\8c назвÑ\83 Ñ\81Ñ\82оÑ\80Ñ\96нки Ñ\89об побаÑ\87иÑ\82и змÑ\96ни на Ñ\81Ñ\82оÑ\80Ñ\96нкаÑ\85 Ñ\8fкÑ\96 поÑ\81илаÑ\8eÑ\82Ñ\8cÑ\81Ñ\8f на неÑ\97, або на Ñ\8fкÑ\96 вона Ñ\81ама поÑ\81илаÑ\94Ñ\82Ñ\8cÑ\81Ñ\8f. (Ð\94лÑ\8f пеÑ\80еглÑ\8fдÑ\83 Ñ\87ленÑ\96в каÑ\82егоÑ\80Ñ\96Ñ\97 вводÑ\8cÑ\82е {{ns:14}}:Ð\9dазва каÑ\82егоÑ\80Ñ\96Ñ\97). Ð\97мÑ\96ни на Ñ\81Ñ\82оÑ\80Ñ\96нкаÑ\85 з [[Special:Watchlist|Ð\92аÑ\88ого СпиÑ\81кÑ\83 Ñ\81поÑ\81Ñ\82еÑ\80еженнÑ\8f]] видÑ\96ленÑ\96 <strong>жиÑ\80ним</strong>.",
"recentchangeslinked-page": "Назва сторінки:",
"recentchangeslinked-to": "Показати зміни на сторінках, пов'язаних з даною",
"recentchanges-page-added-to-category": "[[:$1]] Додано до категорії",
"uploadstash-refresh": "Оновити список файлів",
"uploadstash-thumbnail": "перегляд мініатюри",
"uploadstash-exception": "Не вдалося зберегти завантаження у сховку ($1): «$2».",
+ "uploadstash-bad-path": "Шлях не існує.",
+ "uploadstash-bad-path-invalid": "Неправильний шлях.",
+ "uploadstash-bad-path-unknown-type": "Невідомий тип «$1»",
+ "uploadstash-bad-path-unrecognized-thumb-name": "Нерозпізнана назва мініатюри.",
+ "uploadstash-bad-path-no-handler": "Не знайдено обробник для mime-типу «$1» файлу «$2».",
+ "uploadstash-bad-path-bad-format": "Ключ «$1» в неправильному форматі.",
+ "uploadstash-file-not-found": "Ключ «$1» не знайдено у тимчасовому сховищі.",
+ "uploadstash-file-not-found-no-thumb": "Не вдалось отримати мініатюру.",
+ "uploadstash-file-not-found-no-local-path": "Немає локального шляху для масштабованого елементу.",
+ "uploadstash-file-not-found-no-object": "Не вдалось створити локальний об'єкт файлу для мініатюри.",
+ "uploadstash-file-not-found-no-remote-thumb": "Отримання мініатюри не вдалось: $1\nURL = $2",
+ "uploadstash-file-not-found-missing-content-type": "Відсутній заголовок content-type.",
+ "uploadstash-file-not-found-not-exists": "Не вдалось отримати шлях, або не простий файл.",
+ "uploadstash-file-too-large": "Не вдалось подати файл більший за $1 {{PLURAL:$1|байт|байти|байтів}}.",
+ "uploadstash-not-logged-in": "Немає користувачів, що увійшли до системи, файли повинні належати користувачам.",
+ "uploadstash-wrong-owner": "Цей файл ($1) не належить поточному користувачу.",
+ "uploadstash-no-such-key": "Немає такого файлу ($1), неможливо вилучити.",
+ "uploadstash-no-extension": "Розширення є порожнім.",
+ "uploadstash-zero-length": "Файл нульової довжини.",
"invalid-chunk-offset": "Неприпустимий зсув фрагмента",
"img-auth-accessdenied": "Відмовлено в доступі",
"img-auth-nopathinfo": "Відсутній PATH_INFO.\nВаш сервер не налаштовано для передачі цих даних.\nМожливо, він працює на основі CGI та не підтримує img_auth.\nПерегляньте [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization Відкриття доступу до зображень]",
"import-mapping-namespace": "Імпортувати до простору назв:",
"import-mapping-subpage": "Імпортувати як підсторінки такої сторінки:",
"import-upload-filename": "Назва файлу:",
+ "import-upload-username-prefix": "Інтервікі-префікс:",
+ "import-assign-known-users": "Призначити редагування до локальних користувачів де користувачі з такими іменами існують локально.",
"import-comment": "Примітка:",
"importtext": "Будь ласка, експортуйте сторінку з іншої вікі, використовуючи [[Special:Export|засіб експорту]], збережіть файл, а потім завантажте його сюди.",
"importstart": "Імпорт сторінок…",
"imported-log-entries": "{{PLURAL:$1|Заімпортований $1 запис журналу|Заімпортовані $1 записи журналу|Заімпортовані $1 записів журналу}}.",
"importfailed": "Не вдалося імпортувати: $1",
"importunknownsource": "Невідомий тип імпортованої сторінки",
+ "importnoprefix": "Не вказано інтервікі-префікс",
"importcantopen": "Неможливо відкрити файл імпорту",
"importbadinterwiki": "Невірне інтервікі-посилання",
"importsuccess": "Імпорт виконано!",
"autosumm-blank": "Сторінка очищена",
"autosumm-replace": "Замінено вміст на «$1»",
"autoredircomment": "Перенаправлено на [[$1]]",
+ "autosumm-removed-redirect": "Вилучено перенаправлення на [[$1]]",
+ "autosumm-changed-redirect-target": "Змінено ціль перенаправлення з [[$1]] на [[$2]]",
"autosumm-new": "Створена сторінка: $1",
"autosumm-newblank": "Створити порожню сторінку",
"size-bytes": "$1 {{PLURAL:$1|байт|байти|байтів}}",
"tag-filter": "Фільтр [[Special:Tags|міток]]:",
"tag-filter-submit": "Відфільтрувати",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Мітка|Мітки}}]]: $2)",
- "tag-mw-contentmodelchange": "зміна контентної моделі",
- "tag-mw-contentmodelchange-description": "Редагування, якими була здійснена [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel зміна контентної моделі] сторінки",
+ "tag-mw-contentmodelchange": "зміна моделі вмісту",
+ "tag-mw-contentmodelchange-description": "Редагування, які змінюють [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel модель вмісту] сторінки",
+ "tag-mw-new-redirect": "Нове перенаправлення",
+ "tag-mw-new-redirect-description": "Редагування, що створюють нове перенаправлення, або заміняють сторінку перенаправленням",
+ "tag-mw-removed-redirect": "Вилучено перенаправлення",
+ "tag-mw-removed-redirect-description": "Редагування, що змінюють дійсне перенаправлення на не-перенаправлення",
+ "tag-mw-changed-redirect-target": "Змінено ціль перенаправлення",
+ "tag-mw-changed-redirect-target-description": "Редагування, що змінюють ціль перенаправлення",
+ "tag-mw-blank": "Очищення",
+ "tag-mw-blank-description": "Редагування, що очищують сторінку",
+ "tag-mw-replace": "Замінено",
+ "tag-mw-replace-description": "Редагування, що вилучають понад 90% вмісту сторінки",
+ "tag-mw-rollback": "Відкіт",
+ "tag-mw-rollback-description": "Редагування, що відкидають попередні правки використовуючи посилання відкоту",
+ "tag-mw-undo": "Скасування",
+ "tag-mw-undo-description": "Редагування, що скасовують попередні правки використовуючи посилання скасування",
"tags-title": "Мітки",
"tags-intro": "На цій сторінці наведений список міток, якими програмне забезпечення помічає редагування, а також значення цих міток.",
"tags-tag": "Назва мітки",
"rcfilters-preference-label": "隐藏改进的最近更改版本",
"rcfilters-preference-help": "返回到2017年界面重新设计版,并重新添加这以后新增的工具。",
"rcfilters-filter-showlinkedfrom-label": "显示链接自该页面的页面上的更改",
- "rcfilters-filter-showlinkedfrom-option-label": "显示链接<strong>自</strong>某一页面的页面上的更改",
+ "rcfilters-filter-showlinkedfrom-option-label": "<strong>链接自</strong>选定页面的页面",
"rcfilters-filter-showlinkedto-label": "显示链接到该页面的页面上的更改",
- "rcfilters-filter-showlinkedto-option-label": "显示链接<strong>到</strong>某一页面的页面上的更改",
+ "rcfilters-filter-showlinkedto-option-label": "<strong>链接到</strong>选定页面的页面",
"rcfilters-target-page-placeholder": "输入页面名称",
"rcnotefrom": "下面{{PLURAL:$5|是}}<strong>$3 $4</strong>之后的更改(最多显示<strong>$1</strong>个)。",
"rclistfromreset": "重置时间选择",
$.each( this.groups, function ( name, model ) {
if ( model.isSticky() ) {
- $.extend( true, result, model.getDefaultParams() );
+ $.extend( true, result, model.getParamRepresentation() );
}
} );
use MediaWiki\Services\SalvageableService;
use MediaWiki\Services\ServiceDisabledException;
use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
/**
* @covers MediaWiki\MediaWikiServices
'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ],
'ShellCommandFactory' => [ 'ShellCommandFactory', CommandFactory::class ],
+ 'BlobStore' => [ 'BlobStore', BlobStore::class ],
+ '_SqlBlobStore' => [ '_SqlBlobStore', SqlBlobStore::class ],
+ 'RevisionStore' => [ 'RevisionStore', RevisionStore::class ],
];
}
<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\IncompleteRevisionException;
+use MediaWiki\Storage\RevisionRecord;
/**
* RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
MWNamespace::clearCaches();
// Reset namespace cache
$wgContLang->resetNamespaces();
+
if ( !$this->testPage ) {
/**
* We have to create a new page for each subclass as the page creation may result
$props['text'] = 'Lorem Ipsum';
}
+ if ( !isset( $props['user_text'] ) ) {
+ $props['user_text'] = 'Tester';
+ }
+
+ if ( !isset( $props['user'] ) ) {
+ $props['user'] = 0;
+ }
+
if ( !isset( $props['comment'] ) ) {
$props['comment'] = 'just a test';
}
$props['page'] = $this->testPage->getId();
}
+ if ( !isset( $props['content_model'] ) ) {
+ $props['content_model'] = CONTENT_MODEL_WIKITEXT;
+ }
+
$rev = new Revision( $props );
$dbw = wfGetDB( DB_MASTER );
$revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
$this->assertInternalType( 'integer', $revId );
- $this->assertInternalType( 'integer', $rev->getTextId() );
$this->assertSame( $revId, $rev->getId() );
+ // getTextId() must be an int!
+ $this->assertInternalType( 'integer', $rev->getTextId() );
+
+ $mainSlot = $rev->getRevisionRecord()->getSlot( 'main', RevisionRecord::RAW );
+
+ // we currently only support storage in the text table
+ $textId = MediaWikiServices::getInstance()
+ ->getBlobStore()
+ ->getTextIdFromAddress( $mainSlot->getAddress() );
+
$this->assertSelect(
'text',
[ 'old_id', 'old_text' ],
- "old_id = {$rev->getTextId()}",
- [ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
+ "old_id = $textId",
+ [ [ strval( $textId ), 'Revision Text' ] ]
);
$this->assertSelect(
'revision',
[ [
strval( $rev->getId() ),
strval( $this->testPage->getId() ),
- strval( $rev->getTextId() ),
+ strval( $textId ),
'0',
'0',
'0',
// If an ExternalStore is set don't use it.
$this->setMwGlobals( 'wgDefaultExternalStore', false );
$this->setExpectedException(
- MWException::class,
- "Cannot insert revision: page ID must be nonzero"
+ IncompleteRevisionException::class,
+ "rev_page field must not be 0!"
);
- $rev = new Revision( [] );
+ $title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
+ $rev = new Revision( [], 0, $title );
$rev->insertOn( wfGetDB( DB_MASTER ) );
}
return $f + [ 'ar_namespace', 'ar_title' ];
},
];
+ yield [
+ function ( $f ) {
+ unset( $f['ar_text'] );
+ return $f;
+ },
+ ];
yield [
function ( $f ) {
unset( $f['ar_text_id'] );
return $f;
},
];
+ yield [
+ function ( $f ) {
+ unset( $f['ar_page_id'] );
+ return $f;
+ },
+ ];
+ yield [
+ function ( $f ) {
+ unset( $f['ar_parent_id'] );
+ return $f;
+ },
+ ];
+ yield [
+ function ( $f ) {
+ unset( $f['ar_rev_id'] );
+ return $f;
+ },
+ ];
+ yield [
+ function ( $f ) {
+ unset( $f['ar_sha1'] );
+ return $f;
+ },
+ ];
}
/**
* @covers Revision::newFromArchiveRow
*/
public function testNewFromArchiveRow( $selectModifier ) {
+ $services = MediaWikiServices::getInstance();
+
+ $store = new RevisionStore(
+ $services->getDBLoadBalancer(),
+ $services->getService( '_SqlBlobStore' ),
+ $services->getMainWANObjectCache()
+ );
+
+ $store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
+ $this->setService( 'RevisionStore', $store );
+
$page = $this->createPage(
'RevisionStorageTest_testNewFromArchiveRow',
'Lorem Ipsum',
$row = $res->fetchObject();
$res->free();
+ // MCR migration note: $row is now required to contain ar_title and ar_namespace.
+ // Alternatively, a Title object can be passed to RevisionStore::newRevisionFromArchiveRow
$rev = Revision::newFromArchiveRow( $row );
$this->assertRevEquals( $orig, $rev );
$row = $res->fetchObject();
$res->free();
- $rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
+ $rev = Revision::newFromArchiveRow( $row, [ 'comment_text' => 'SOMEOVERRIDE' ] );
$this->assertNotEquals( $orig->getComment(), $rev->getComment() );
$this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
* @covers Revision::newFromPageId
*/
public function testNewFromPageIdWithNotLatestId() {
- $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+ $content = new WikitextContent( __METHOD__ );
+ $this->testPage->doEditContent( $content, __METHOD__ );
$rev = Revision::newFromPageId(
$this->testPage->getId(),
$this->testPage->getRevision()->getPrevious()->getId()
$this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
$id = $this->testPage->getRevision()->getId();
+ $this->hideDeprecated( 'Revision::fetchRevision' );
$res = Revision::fetchRevision( $this->testPage->getTitle() );
# note: order is unspecified
$rows[$row->rev_id] = $row;
}
- $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
- $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
+ $this->assertEmpty( $rows, 'expected empty set' );
}
/**
'new null revision should have a different id from the original revision' );
$this->assertEquals( $orig->getTextId(), $rev->getTextId(),
'new null revision should have the same text id as the original revision' );
+ $this->assertEquals( $orig->getSha1(), $rev->getSha1(),
+ 'new null revision should have the same SHA1 as the original revision' );
+ $this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ),
+ 'new null revision should have the same content as the original revision' );
$this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
}
'user' => $userA->getId(),
'text' => 'zero',
'content_model' => CONTENT_MODEL_WIKITEXT,
- 'summary' => 'edit zero'
+ 'comment' => 'edit zero'
] );
$revisions[0]->insertOn( $dbw );
'user' => $userA->getId(),
'text' => 'one',
'content_model' => CONTENT_MODEL_WIKITEXT,
- 'summary' => 'edit one'
+ 'comment' => 'edit one'
] );
$revisions[1]->insertOn( $dbw );
'user' => $userB->getId(),
'text' => 'two',
'content_model' => CONTENT_MODEL_WIKITEXT,
- 'summary' => 'edit two'
+ 'comment' => 'edit two'
] );
$revisions[2]->insertOn( $dbw );
'user' => $userA->getId(),
'text' => 'three',
'content_model' => CONTENT_MODEL_WIKITEXT,
- 'summary' => 'edit three'
+ 'comment' => 'edit three'
] );
$revisions[3]->insertOn( $dbw );
'user' => $userA->getId(),
'text' => 'zero',
'content_model' => CONTENT_MODEL_WIKITEXT,
- 'summary' => 'edit four'
+ 'comment' => 'edit four'
] );
$revisions[4]->insertOn( $dbw );
// test it ---------------------------------
$since = $revisions[$sinceIdx]->getTimestamp();
+ $allRows = iterator_to_array( $dbw->select(
+ 'revision',
+ [ 'rev_id', 'rev_timestamp', 'rev_user' ],
+ [
+ 'rev_page' => $page->getId(),
+ //'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) )
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ]
+ ) );
+
$wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
$this->assertEquals( $expectedLast, $wasLast );
'text_id' => 123456789, // not in the test DB
] );
+ MediaWiki\suppressWarnings(); // bad text_id will trigger a warning.
+
$this->assertNull( $rev->getContent(),
"getContent() should return null if the revision's text blob could not be loaded." );
// NOTE: check this twice, once for lazy initialization, and once with the cached value.
$this->assertNull( $rev->getContent(),
"getContent() should return null if the revision's text blob could not be loaded." );
+
+ MediaWiki\suppressWarnings( 'end' );
}
public function provideGetSize() {
*/
public function testLoadFromId() {
$rev = $this->testPage->getRevision();
+ $this->hideDeprecated( 'Revision::loadFromId' );
$this->assertRevEquals(
$rev,
Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
$rev[1] = $this->testPage->getLatest();
$this->assertSame(
- [ $rev[1] => strval( $textLength ) ],
+ [ $rev[1] => $textLength ],
Revision::getParentLengths(
wfGetDB( DB_MASTER ),
[ $rev[1] ]
$rev[2] = $this->testPage->getLatest();
$this->assertSame(
- [ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
+ [ $rev[1] => $textOneLength, $rev[2] => $textTwoLength ],
Revision::getParentLengths(
wfGetDB( DB_MASTER ),
[ $rev[1], $rev[2] ]
);
}
- /**
- * @covers Revision::getTitle
- */
- public function testGetTitle_forBadRevision() {
- $rev = new Revision( [] );
- $this->assertNull( $rev->getTitle() );
- }
-
/**
* @covers Revision::isMinor
*/
$rev = $this->testPage->getRevision();
// Clear any previous cache for the revision during creation
- $key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
+ $key = $cache->makeGlobalKey( 'revision-row-1.29',
+ $db->getDomainID(),
+ $rev->getPage(),
+ $rev->getId()
+ );
$cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
$this->assertFalse( $cache->get( $key ) );
// Get the new revision and make sure it is in the cache and correct
$newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
$this->assertRevEquals( $rev, $newRev );
- $this->assertRevEquals( $rev, $cache->get( $key ) );
+
+ $cachedRow = $cache->get( $key );
+ $this->assertNotFalse( $cachedRow );
+ $this->assertEquals( $rev->getId(), $cachedRow->rev_id );
}
public function provideUserCanBitfield() {
]
);
$user = $this->getTestUser( $userGroups )->getUser();
- $revision = new Revision( [ 'deleted' => $bitField ] );
+ $revision = new Revision( [ 'deleted' => $bitField ], 0, $this->testPage->getTitle() );
$this->assertSame(
$expected,
<?php
-use Wikimedia\TestingAccessWrapper;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
/**
* Test cases in RevisionTest should not interact with the Database.
/**
* @dataProvider provideConstructFromArray
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*/
- public function testConstructFromArray( array $rowArray ) {
- $rev = new Revision( $rowArray );
+ public function testConstructFromArray( $rowArray ) {
+ $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertNotNull( $rev->getContent(), 'no content object available' );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
/**
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromEmptyArray() {
$rev = new Revision( [], 0, $this->getMockTitle() );
99,
'SomeTextUserName',
];
- // Note: the below XXX test cases are odd and probably result in unexpected behaviour if used
- // in production code.
- yield 'XXX: user text only' => [
+ yield 'user text only' => [
[
'content' => new JavaScriptContent( 'hello world.' ),
'user_text' => '111.111.111.111',
],
- null,
+ 0,
'111.111.111.111',
];
- yield 'XXX: user id only' => [
- [
- 'content' => new JavaScriptContent( 'hello world.' ),
- 'user' => 9989,
- ],
- 9989,
- null,
- ];
}
/**
* @dataProvider provideConstructFromArray_userSetAsExpected
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*
* @param array $rowArray
* @param mixed $expectedUserId null to expect the current wgUser ID
$expectedUserName = $testUser->getName();
}
- $rev = new Revision( $rowArray );
+ $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertEquals( $expectedUserId, $rev->getUser() );
$this->assertEquals( $expectedUserName, $rev->getUserText() );
}
[
'content' => new WikitextContent( 'GOAT' ),
'text_id' => 'someid',
- ],
+ ],
new MWException( "Text already stored in external store (id someid), " .
"can't serialize content object" )
];
+ yield 'unknown user id and no user name' => [
+ [
+ 'content' => new JavaScriptContent( 'hello world.' ),
+ 'user' => 9989,
+ ],
+ new MWException( 'user_text not given, and unknown user ID 9989' )
+ ];
yield 'with bad content object (class)' => [
[ 'content' => new stdClass() ],
- new MWException( '`content` field must contain a Content object.' )
+ new MWException( 'content field must contain a Content object.' )
];
yield 'with bad content object (string)' => [
[ 'content' => 'ImAGoat' ],
- new MWException( '`content` field must contain a Content object.' )
+ new MWException( 'content field must contain a Content object.' )
];
yield 'bad row format' => [
'imastring, not a row',
- new MWException( 'Revision constructor passed invalid row format.' )
+ new InvalidArgumentException(
+ '$row must be a row object, an associative array, or a RevisionRecord'
+ )
];
}
/**
* @dataProvider provideConstructFromArrayThrowsExceptions
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
$this->setExpectedException(
$expectedException->getMessage(),
$expectedException->getCode()
);
- new Revision( $rowArray );
+ new Revision( $rowArray, 0, $this->getMockTitle() );
}
/**
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromNothing() {
- $rev = new Revision( [] );
- $this->assertNull( $rev->getId(), 'getId()' );
+ $this->setExpectedException(
+ InvalidArgumentException::class
+ );
+ new Revision( [] );
}
public function provideConstructFromRow() {
yield 'Full construction' => [
[
- 'rev_id' => '2',
- 'rev_page' => '1',
+ 'rev_id' => '42',
+ 'rev_page' => '23',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
'rev_content_model' => 'GOATMODEL',
],
function ( RevisionTest $testCase, Revision $rev ) {
- $testCase->assertSame( 2, $rev->getId() );
- $testCase->assertSame( 1, $rev->getPage() );
+ $testCase->assertSame( 42, $rev->getId() );
+ $testCase->assertSame( 23, $rev->getPage() );
$testCase->assertSame( 2, $rev->getTextId() );
$testCase->assertSame( '20171017114835', $rev->getTimestamp() );
$testCase->assertSame( '127.0.0.1', $rev->getUserText() );
$testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
}
];
- yield 'null fields' => [
+ yield 'default field values' => [
[
- 'rev_id' => '2',
- 'rev_page' => '1',
+ 'rev_id' => '42',
+ 'rev_page' => '23',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
'rev_comment_cid' => null,
],
function ( RevisionTest $testCase, Revision $rev ) {
- $testCase->assertNull( $rev->getSize() );
- $testCase->assertNull( $rev->getParentId() );
- $testCase->assertNull( $rev->getSha1() );
- $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
- $testCase->assertSame( 'wikitext', $rev->getContentModel() );
+ // parent ID may be null
+ $testCase->assertSame( null, $rev->getParentId(), 'revision id' );
+
+ // given fields
+ $testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' );
+ $testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' );
+ $testCase->assertSame( $rev->getUser(), 0, 'user id' );
+ $testCase->assertSame( $rev->getComment(), 'Goat Comment!' );
+ $testCase->assertSame( false, $rev->isMinor(), 'minor edit' );
+ $testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' );
+
+ // computed fields
+ $testCase->assertNotNull( $rev->getSize(), 'size' );
+ $testCase->assertNotNull( $rev->getSha1(), 'hash' );
+
+ // NOTE: model and format will be detected based on the namespace of the (mock) title
+ $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat(), 'format' );
+ $testCase->assertSame( 'wikitext', $rev->getContentModel(), 'model' );
}
];
}
/**
* @dataProvider provideConstructFromRow
* @covers Revision::__construct
- * @covers Revision::constructFromRowArray
+ * @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromRow( array $arrayData, $assertions ) {
+ $data = 'Hello goat.'; // needs to match model and format
+
+ $blobStore = $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $blobStore->method( 'getBlob' )
+ ->will( $this->returnValue( $data ) );
+
+ $blobStore->method( 'getTextIdFromAddress' )
+ ->will( $this->returnCallback(
+ function ( $address ) {
+ // Turn "tt:1234" into 12345.
+ // Note that this must be functional so we can test getTextId().
+ // Ideally, we'd un-mock getTextIdFromAddress and use its actual implementation.
+ $parts = explode( ':', $address );
+ return (int)array_pop( $parts );
+ }
+ ) );
+
+ // Note override internal service, so RevisionStore uses it as well.
+ $this->setService( '_SqlBlobStore', $blobStore );
+
$row = (object)$arrayData;
- $rev = new Revision( $row );
+ $rev = new Revision( $row, 0, $this->getMockTitle() );
$assertions( $this, $rev );
}
* @covers Revision::getId
*/
public function testGetId( $rowArray, $expectedId ) {
- $rev = new Revision( $rowArray );
+ $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertEquals( $expectedId, $rev->getId() );
}
* @covers Revision::setId
*/
public function testSetId( $input, $expected ) {
- $rev = new Revision( [] );
+ $rev = new Revision( [], 0, $this->getMockTitle() );
$rev->setId( $input );
$this->assertSame( $expected, $rev->getId() );
}
* @covers Revision::setUserIdAndName
*/
public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
- $rev = new Revision( [] );
+ $rev = new Revision( [], 0, $this->getMockTitle() );
$rev->setUserIdAndName( $inputId, $name );
$this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
$this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
* @covers Revision::getTextId()
*/
public function testGetTextId( $rowArray, $expected ) {
- $rev = new Revision( $rowArray );
+ $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertSame( $expected, $rev->getTextId() );
}
* @covers Revision::getParentId()
*/
public function testGetParentId( $rowArray, $expected ) {
- $rev = new Revision( $rowArray );
+ $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertSame( $expected, $rev->getParentId() );
}
$this->testGetRevisionText( $expected, $rowData );
}
+ private function getWANObjectCache() {
+ return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ }
+
+ /**
+ * @return SqlBlobStore
+ */
+ private function getBlobStore() {
+ /** @var LoadBalancer $lb */
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $cache = $this->getWANObjectCache();
+
+ $blobStore = new SqlBlobStore( $lb, $cache );
+ return $blobStore;
+ }
+
+ /**
+ * @return RevisionStore
+ */
+ private function getRevisionStore() {
+ /** @var LoadBalancer $lb */
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $cache = $this->getWANObjectCache();
+
+ $blobStore = new RevisionStore( $lb, $this->getBlobStore(), $cache );
+ return $blobStore;
+ }
+
public function provideGetRevisionTextWithLegacyEncoding() {
yield 'Utf8Native' => [
"Wiki est l'\xc3\xa9cole superieur !",
+ 'fr',
'iso-8859-1',
[
'old_flags' => 'utf-8',
];
yield 'Utf8Legacy' => [
"Wiki est l'\xc3\xa9cole superieur !",
+ 'fr',
'iso-8859-1',
[
'old_flags' => '',
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithLegacyEncoding
*/
- public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
- $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
+ public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
+ $blobStore = $this->getBlobStore();
+ $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
+ $this->setService( 'BlobStore', $blobStore );
+
$this->testGetRevisionText( $expected, $rowData );
}
*/
yield 'Utf8NativeGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
+ 'fr',
'iso-8859-1',
[
'old_flags' => 'gzip,utf-8',
];
yield 'Utf8LegacyGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
+ 'fr',
'iso-8859-1',
[
'old_flags' => 'gzip',
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
*/
- public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
+ public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
$this->checkPHPExtension( 'zlib' );
- $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
+
+ $blobStore = $this->getBlobStore();
+ $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
+ $this->setService( 'BlobStore', $blobStore );
+
$this->testGetRevisionText( $expected, $rowData );
}
*/
public function testCompressRevisionTextUtf8Gzip() {
$this->checkPHPExtension( 'zlib' );
- $this->setMwGlobals( 'wgCompressRevisions', true );
+
+ $blobStore = $this->getBlobStore();
+ $blobStore->setCompressBlobs( true );
+ $this->setService( 'BlobStore', $blobStore );
$row = new stdClass;
$row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
Revision::getRevisionText( $row ), "getRevisionText" );
}
- public function provideFetchFromConds() {
- yield [ 0, [] ];
- yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
- }
-
/**
- * @dataProvider provideFetchFromConds
- * @covers Revision::fetchFromConds
+ * @covers Revision::loadFromTitle
*/
- public function testFetchFromConds( $flags, array $options ) {
- $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
- $conditions = [ 'conditionsArray' ];
+ public function testLoadFromTitle() {
+ $title = $this->getMockTitle();
+
+ $conditions = [
+ 'rev_id=page_latest',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ];
+
+ $row = (object)[
+ 'rev_id' => '42',
+ 'rev_page' => $title->getArticleID(),
+ 'rev_text_id' => '2',
+ 'rev_timestamp' => '20171017114835',
+ 'rev_user_text' => '127.0.0.1',
+ 'rev_user' => '0',
+ 'rev_minor_edit' => '0',
+ 'rev_deleted' => '0',
+ 'rev_len' => '46',
+ 'rev_parent_id' => '1',
+ 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'rev_comment_text' => 'Goat Comment!',
+ 'rev_comment_data' => null,
+ 'rev_comment_cid' => null,
+ 'rev_content_format' => 'GOATFORMAT',
+ 'rev_content_model' => 'GOATMODEL',
+ ];
$db = $this->getMock( IDatabase::class );
+ $db->expects( $this->any() )
+ ->method( 'getDomainId' )
+ ->will( $this->returnValue( wfWikiID() ) );
$db->expects( $this->once() )
->method( 'selectRow' )
->with(
$this->isType( 'array' ),
$this->equalTo( $conditions ),
// Method name
- $this->equalTo( 'Revision::fetchFromConds' ),
- $this->equalTo( $options ),
+ $this->stringContains( 'fetchRevisionRowFromConds' ),
+ // We don't really care about the options here
+ $this->isType( 'array' ),
// We don't really care about the join conds are they come from the joinCond methods
$this->isType( 'array' )
)
- ->willReturn( 'RETURNVALUE' );
-
- $wrapper = TestingAccessWrapper::newFromClass( Revision::class );
- $result = $wrapper->fetchFromConds( $db, $conditions, $flags );
-
- $this->assertEquals( 'RETURNVALUE', $result );
+ ->willReturn( $row );
+
+ $revision = Revision::loadFromTitle( $db, $title );
+
+ $this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() );
+ $this->assertEquals( $row->rev_id, $revision->getId() );
+ $this->assertEquals( $row->rev_len, $revision->getSize() );
+ $this->assertEquals( $row->rev_sha1, $revision->getSha1() );
+ $this->assertEquals( $row->rev_parent_id, $revision->getParentId() );
+ $this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() );
+ $this->assertEquals( $row->rev_comment_text, $revision->getComment() );
+ $this->assertEquals( $row->rev_user_text, $revision->getUserText() );
}
public function provideDecompressRevisionText() {
* @param mixed $expected
*/
public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
- $this->setMwGlobals( 'wgLegacyEncoding', $legacyEncoding );
- $this->setMwGlobals( 'wgLanguageCode', 'en' );
+ $blobStore = $this->getBlobStore();
+ if ( $legacyEncoding ) {
+ $blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) );
+ }
+
+ $this->setService( 'BlobStore', $blobStore );
$this->assertSame(
$expected,
Revision::decompressRevisionText( $text, $flags )
* @covers Revision::getRevisionText
*/
public function testGetRevisionText_external_oldId() {
- $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ $cache = $this->getWANObjectCache();
$this->setService( 'MainWANObjectCache', $cache );
+
$this->setService(
'ExternalStoreFactory',
new ExternalStoreFactory( [ 'ForTesting' ] )
);
- $cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $blobStore = new SqlBlobStore( $lb, $cache );
+ $this->setService( 'BlobStore', $blobStore );
$this->assertSame(
'AAAABBAAA',
]
)
);
+
+ $cacheKey = $cache->makeKey( 'revisiontext', 'textid', 'tt:7777' );
$this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
}
'fields' => [
'ar_id',
'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
+ 'ar_namespace',
+ 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
*/
public function testGetArchiveQueryInfo( $globals, $expected ) {
$this->setMwGlobals( $globals );
+
+ $revisionStore = $this->getRevisionStore();
+ $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
+ $this->setService( 'RevisionStore', $revisionStore );
+
$this->assertEquals(
$expected,
Revision::getArchiveQueryInfo()
*/
public function testGetQueryInfo( $globals, $options, $expected ) {
$this->setMwGlobals( $globals );
+
+ $revisionStore = $this->getRevisionStore();
+ $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
+ $this->setService( 'RevisionStore', $revisionStore );
+
$this->assertEquals(
$expected,
Revision::getQueryInfo( $options )
<?php
+use Wikimedia\Rdbms\DBError;
+use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
/**
* @file
*/
class LoadBalancerTest extends MediaWikiTestCase {
- public function testLBSimpleServer() {
+ public function testWithoutReplica() {
global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
$servers = [
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
+ $this->assertWriteAllowed( $dbw );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
- $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
+ $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
$dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
$this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
$lb->closeAll();
}
- public function testLBSimpleServers() {
+ public function testWithReplica() {
global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
$servers = [
'load' => 0,
'flags' => DBO_TRX // REPEATABLE-READ for consistency
],
- [ // emulated slave
+ [ // emulated replica
'host' => $wgDBserver,
'dbname' => $wgDBname,
'user' => $wgDBuser,
$dbw->getLBInfo( 'clusterMasterHost' ),
'cluster master set' );
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
+ $this->assertWriteAllowed( $dbw );
$dbr = $lb->getConnection( DB_REPLICA );
- $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows as slave' );
+ $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
$this->assertEquals(
( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
$dbr->getLBInfo( 'clusterMasterHost' ),
'cluster master set' );
- $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
+ $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
+ $this->assertWriteForbidden( $dbr );
$dbwAuto = $lb->getConnection( DB_MASTER, [], false, $lb::CONN_TRX_AUTO );
$this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No DBO_TRX with CONN_TRX_AUTO" );
$lb->closeAll();
}
+
+ private function assertWriteForbidden( IDatabase $db ) {
+ try {
+ $db->delete( 'user', [ 'user_id' => 57634126 ], 'TEST' );
+ $this->fail( 'Write operation should have failed!' );
+ } catch ( DBError $ex ) {
+ // check that the exception message contains "Write operation"
+ $constraint = new PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
+
+ if ( !$constraint->evaluate( $ex->getMessage(), '', true ) ) {
+ // re-throw original error, to preserve stack trace
+ throw $ex;
+ }
+ } finally {
+ $db->rollback( __METHOD__, 'flush' );
+ }
+ }
+
+ private function assertWriteAllowed( IDatabase $db ) {
+ try {
+ $this->assertNotSame( false, $db->delete( 'user', [ 'user_id' => 57634126 ] ) );
+ } finally {
+ $db->rollback( __METHOD__, 'flush' );
+ }
+ }
+
}
assert.equal( href, '/wiki/#Fragment', 'empty title with fragment' );
href = util.getUrl( '#Fragment', { action: 'edit' } );
- assert.equal( href, '/w/index.php?action=edit#Fragment', 'epmty title with query string and fragment' );
+ assert.equal( href, '/w/index.php?action=edit#Fragment', 'empty title with query string and fragment' );
href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } );
assert.equal( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_.C3.84', 'title with query string, fragment, and special characters' );