wikimedia /
mediawiki
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * Representation of a page version. |
||
| 4 | * |
||
| 5 | * This program is free software; you can redistribute it and/or modify |
||
| 6 | * it under the terms of the GNU General Public License as published by |
||
| 7 | * the Free Software Foundation; either version 2 of the License, or |
||
| 8 | * (at your option) any later version. |
||
| 9 | * |
||
| 10 | * This program is distributed in the hope that it will be useful, |
||
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 13 | * GNU General Public License for more details. |
||
| 14 | * |
||
| 15 | * You should have received a copy of the GNU General Public License along |
||
| 16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
| 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
| 18 | * http://www.gnu.org/copyleft/gpl.html |
||
| 19 | * |
||
| 20 | * @file |
||
| 21 | */ |
||
| 22 | use MediaWiki\Linker\LinkTarget; |
||
| 23 | use MediaWiki\MediaWikiServices; |
||
| 24 | |||
| 25 | /** |
||
| 26 | * @todo document |
||
| 27 | */ |
||
| 28 | class Revision implements IDBAccessObject { |
||
| 29 | /** @var int|null */ |
||
| 30 | protected $mId; |
||
| 31 | /** @var int|null */ |
||
| 32 | protected $mPage; |
||
| 33 | /** @var string */ |
||
| 34 | protected $mUserText; |
||
| 35 | /** @var string */ |
||
| 36 | protected $mOrigUserText; |
||
| 37 | /** @var int */ |
||
| 38 | protected $mUser; |
||
| 39 | /** @var bool */ |
||
| 40 | protected $mMinorEdit; |
||
| 41 | /** @var string */ |
||
| 42 | protected $mTimestamp; |
||
| 43 | /** @var int */ |
||
| 44 | protected $mDeleted; |
||
| 45 | /** @var int */ |
||
| 46 | protected $mSize; |
||
| 47 | /** @var string */ |
||
| 48 | protected $mSha1; |
||
| 49 | /** @var int */ |
||
| 50 | protected $mParentId; |
||
| 51 | /** @var string */ |
||
| 52 | protected $mComment; |
||
| 53 | /** @var string */ |
||
| 54 | protected $mText; |
||
| 55 | /** @var int */ |
||
| 56 | protected $mTextId; |
||
| 57 | /** @var int */ |
||
| 58 | protected $mUnpatrolled; |
||
| 59 | |||
| 60 | /** @var stdClass|null */ |
||
| 61 | protected $mTextRow; |
||
| 62 | |||
| 63 | /** @var null|Title */ |
||
| 64 | protected $mTitle; |
||
| 65 | /** @var bool */ |
||
| 66 | protected $mCurrent; |
||
| 67 | /** @var string */ |
||
| 68 | protected $mContentModel; |
||
| 69 | /** @var string */ |
||
| 70 | protected $mContentFormat; |
||
| 71 | |||
| 72 | /** @var Content|null|bool */ |
||
| 73 | protected $mContent; |
||
| 74 | /** @var null|ContentHandler */ |
||
| 75 | protected $mContentHandler; |
||
| 76 | |||
| 77 | /** @var int */ |
||
| 78 | protected $mQueryFlags = 0; |
||
| 79 | /** @var bool Used for cached values to reload user text and rev_deleted */ |
||
| 80 | protected $mRefreshMutableFields = false; |
||
| 81 | /** @var string Wiki ID; false means the current wiki */ |
||
| 82 | protected $mWiki = false; |
||
| 83 | |||
| 84 | // Revision deletion constants |
||
| 85 | const DELETED_TEXT = 1; |
||
| 86 | const DELETED_COMMENT = 2; |
||
| 87 | const DELETED_USER = 4; |
||
| 88 | const DELETED_RESTRICTED = 8; |
||
| 89 | const SUPPRESSED_USER = 12; // convenience |
||
| 90 | const SUPPRESSED_ALL = 15; // convenience |
||
| 91 | |||
| 92 | // Audience options for accessors |
||
| 93 | const FOR_PUBLIC = 1; |
||
| 94 | const FOR_THIS_USER = 2; |
||
| 95 | const RAW = 3; |
||
| 96 | |||
| 97 | const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count |
||
| 98 | |||
| 99 | /** |
||
| 100 | * Load a page revision from a given revision ID number. |
||
| 101 | * Returns null if no such revision can be found. |
||
| 102 | * |
||
| 103 | * $flags include: |
||
| 104 | * Revision::READ_LATEST : Select the data from the master |
||
| 105 | * Revision::READ_LOCKING : Select & lock the data from the master |
||
| 106 | * |
||
| 107 | * @param int $id |
||
| 108 | * @param int $flags (optional) |
||
| 109 | * @return Revision|null |
||
| 110 | */ |
||
| 111 | public static function newFromId( $id, $flags = 0 ) { |
||
| 112 | return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags ); |
||
| 113 | } |
||
| 114 | |||
| 115 | /** |
||
| 116 | * Load either the current, or a specified, revision |
||
| 117 | * that's attached to a given link target. If not attached |
||
| 118 | * to that link target, will return null. |
||
| 119 | * |
||
| 120 | * $flags include: |
||
| 121 | * Revision::READ_LATEST : Select the data from the master |
||
| 122 | * Revision::READ_LOCKING : Select & lock the data from the master |
||
| 123 | * |
||
| 124 | * @param LinkTarget $linkTarget |
||
| 125 | * @param int $id (optional) |
||
| 126 | * @param int $flags Bitfield (optional) |
||
| 127 | * @return Revision|null |
||
| 128 | */ |
||
| 129 | View Code Duplication | public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) { |
|
| 130 | $conds = [ |
||
| 131 | 'page_namespace' => $linkTarget->getNamespace(), |
||
| 132 | 'page_title' => $linkTarget->getDBkey() |
||
| 133 | ]; |
||
| 134 | if ( $id ) { |
||
| 135 | // Use the specified ID |
||
| 136 | $conds['rev_id'] = $id; |
||
| 137 | return self::newFromConds( $conds, $flags ); |
||
| 138 | } else { |
||
| 139 | // Use a join to get the latest revision |
||
| 140 | $conds[] = 'rev_id=page_latest'; |
||
| 141 | $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA ); |
||
| 142 | return self::loadFromConds( $db, $conds, $flags ); |
||
| 143 | } |
||
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Load either the current, or a specified, revision |
||
| 148 | * that's attached to a given page ID. |
||
| 149 | * Returns null if no such revision can be found. |
||
| 150 | * |
||
| 151 | * $flags include: |
||
| 152 | * Revision::READ_LATEST : Select the data from the master (since 1.20) |
||
| 153 | * Revision::READ_LOCKING : Select & lock the data from the master |
||
| 154 | * |
||
| 155 | * @param int $pageId |
||
| 156 | * @param int $revId (optional) |
||
| 157 | * @param int $flags Bitfield (optional) |
||
| 158 | * @return Revision|null |
||
| 159 | */ |
||
| 160 | View Code Duplication | public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) { |
|
| 161 | $conds = [ 'page_id' => $pageId ]; |
||
| 162 | if ( $revId ) { |
||
| 163 | $conds['rev_id'] = $revId; |
||
| 164 | return self::newFromConds( $conds, $flags ); |
||
| 165 | } else { |
||
| 166 | // Use a join to get the latest revision |
||
| 167 | $conds[] = 'rev_id = page_latest'; |
||
| 168 | $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA ); |
||
| 169 | return self::loadFromConds( $db, $conds, $flags ); |
||
| 170 | } |
||
| 171 | } |
||
| 172 | |||
| 173 | /** |
||
| 174 | * Make a fake revision object from an archive table row. This is queried |
||
| 175 | * for permissions or even inserted (as in Special:Undelete) |
||
| 176 | * @todo FIXME: Should be a subclass for RevisionDelete. [TS] |
||
| 177 | * |
||
| 178 | * @param object $row |
||
| 179 | * @param array $overrides |
||
| 180 | * |
||
| 181 | * @throws MWException |
||
| 182 | * @return Revision |
||
| 183 | */ |
||
| 184 | public static function newFromArchiveRow( $row, $overrides = [] ) { |
||
| 185 | global $wgContentHandlerUseDB; |
||
| 186 | |||
| 187 | $attribs = $overrides + [ |
||
| 188 | 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null, |
||
| 189 | 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null, |
||
| 190 | 'comment' => $row->ar_comment, |
||
| 191 | 'user' => $row->ar_user, |
||
| 192 | 'user_text' => $row->ar_user_text, |
||
| 193 | 'timestamp' => $row->ar_timestamp, |
||
| 194 | 'minor_edit' => $row->ar_minor_edit, |
||
| 195 | 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null, |
||
| 196 | 'deleted' => $row->ar_deleted, |
||
| 197 | 'len' => $row->ar_len, |
||
| 198 | 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null, |
||
| 199 | 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null, |
||
| 200 | 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null, |
||
| 201 | ]; |
||
| 202 | |||
| 203 | if ( !$wgContentHandlerUseDB ) { |
||
| 204 | unset( $attribs['content_model'] ); |
||
| 205 | unset( $attribs['content_format'] ); |
||
| 206 | } |
||
| 207 | |||
| 208 | if ( !isset( $attribs['title'] ) |
||
| 209 | && isset( $row->ar_namespace ) |
||
| 210 | && isset( $row->ar_title ) |
||
| 211 | ) { |
||
| 212 | $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title ); |
||
| 213 | } |
||
| 214 | |||
| 215 | if ( isset( $row->ar_text ) && !$row->ar_text_id ) { |
||
| 216 | // Pre-1.5 ar_text row |
||
| 217 | $attribs['text'] = self::getRevisionText( $row, 'ar_' ); |
||
| 218 | if ( $attribs['text'] === false ) { |
||
| 219 | throw new MWException( 'Unable to load text from archive row (possibly bug 22624)' ); |
||
| 220 | } |
||
| 221 | } |
||
| 222 | return new self( $attribs ); |
||
| 223 | } |
||
| 224 | |||
| 225 | /** |
||
| 226 | * @since 1.19 |
||
| 227 | * |
||
| 228 | * @param object $row |
||
| 229 | * @return Revision |
||
| 230 | */ |
||
| 231 | public static function newFromRow( $row ) { |
||
| 232 | return new self( $row ); |
||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * Load a page revision from a given revision ID number. |
||
| 237 | * Returns null if no such revision can be found. |
||
| 238 | * |
||
| 239 | * @param IDatabase $db |
||
| 240 | * @param int $id |
||
| 241 | * @return Revision|null |
||
| 242 | */ |
||
| 243 | public static function loadFromId( $db, $id ) { |
||
| 244 | return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] ); |
||
| 245 | } |
||
| 246 | |||
| 247 | /** |
||
| 248 | * Load either the current, or a specified, revision |
||
| 249 | * that's attached to a given page. If not attached |
||
| 250 | * to that page, will return null. |
||
| 251 | * |
||
| 252 | * @param IDatabase $db |
||
| 253 | * @param int $pageid |
||
| 254 | * @param int $id |
||
| 255 | * @return Revision|null |
||
| 256 | */ |
||
| 257 | public static function loadFromPageId( $db, $pageid, $id = 0 ) { |
||
| 258 | $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ]; |
||
| 259 | if ( $id ) { |
||
| 260 | $conds['rev_id'] = intval( $id ); |
||
| 261 | } else { |
||
| 262 | $conds[] = 'rev_id=page_latest'; |
||
| 263 | } |
||
| 264 | return self::loadFromConds( $db, $conds ); |
||
| 265 | } |
||
| 266 | |||
| 267 | /** |
||
| 268 | * Load either the current, or a specified, revision |
||
| 269 | * that's attached to a given page. If not attached |
||
| 270 | * to that page, will return null. |
||
| 271 | * |
||
| 272 | * @param IDatabase $db |
||
| 273 | * @param Title $title |
||
| 274 | * @param int $id |
||
| 275 | * @return Revision|null |
||
| 276 | */ |
||
| 277 | public static function loadFromTitle( $db, $title, $id = 0 ) { |
||
| 278 | if ( $id ) { |
||
| 279 | $matchId = intval( $id ); |
||
| 280 | } else { |
||
| 281 | $matchId = 'page_latest'; |
||
| 282 | } |
||
| 283 | return self::loadFromConds( $db, |
||
| 284 | [ |
||
| 285 | "rev_id=$matchId", |
||
| 286 | 'page_namespace' => $title->getNamespace(), |
||
| 287 | 'page_title' => $title->getDBkey() |
||
| 288 | ] |
||
| 289 | ); |
||
| 290 | } |
||
| 291 | |||
| 292 | /** |
||
| 293 | * Load the revision for the given title with the given timestamp. |
||
| 294 | * WARNING: Timestamps may in some circumstances not be unique, |
||
| 295 | * so this isn't the best key to use. |
||
| 296 | * |
||
| 297 | * @param IDatabase $db |
||
| 298 | * @param Title $title |
||
| 299 | * @param string $timestamp |
||
| 300 | * @return Revision|null |
||
| 301 | */ |
||
| 302 | public static function loadFromTimestamp( $db, $title, $timestamp ) { |
||
| 303 | return self::loadFromConds( $db, |
||
| 304 | [ |
||
| 305 | 'rev_timestamp' => $db->timestamp( $timestamp ), |
||
| 306 | 'page_namespace' => $title->getNamespace(), |
||
| 307 | 'page_title' => $title->getDBkey() |
||
| 308 | ] |
||
| 309 | ); |
||
| 310 | } |
||
| 311 | |||
| 312 | /** |
||
| 313 | * Given a set of conditions, fetch a revision |
||
| 314 | * |
||
| 315 | * This method is used then a revision ID is qualified and |
||
| 316 | * will incorporate some basic replica DB/master fallback logic |
||
| 317 | * |
||
| 318 | * @param array $conditions |
||
| 319 | * @param int $flags (optional) |
||
| 320 | * @return Revision|null |
||
| 321 | */ |
||
| 322 | private static function newFromConds( $conditions, $flags = 0 ) { |
||
| 323 | $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA ); |
||
| 324 | |||
| 325 | $rev = self::loadFromConds( $db, $conditions, $flags ); |
||
| 326 | // Make sure new pending/committed revision are visibile later on |
||
| 327 | // within web requests to certain avoid bugs like T93866 and T94407. |
||
| 328 | if ( !$rev |
||
| 329 | && !( $flags & self::READ_LATEST ) |
||
| 330 | && wfGetLB()->getServerCount() > 1 |
||
| 331 | && wfGetLB()->hasOrMadeRecentMasterChanges() |
||
| 332 | ) { |
||
| 333 | $flags = self::READ_LATEST; |
||
| 334 | $db = wfGetDB( DB_MASTER ); |
||
| 335 | $rev = self::loadFromConds( $db, $conditions, $flags ); |
||
| 336 | } |
||
| 337 | |||
| 338 | if ( $rev ) { |
||
| 339 | $rev->mQueryFlags = $flags; |
||
| 340 | } |
||
| 341 | |||
| 342 | return $rev; |
||
| 343 | } |
||
| 344 | |||
| 345 | /** |
||
| 346 | * Given a set of conditions, fetch a revision from |
||
| 347 | * the given database connection. |
||
| 348 | * |
||
| 349 | * @param IDatabase $db |
||
| 350 | * @param array $conditions |
||
| 351 | * @param int $flags (optional) |
||
| 352 | * @return Revision|null |
||
| 353 | */ |
||
| 354 | private static function loadFromConds( $db, $conditions, $flags = 0 ) { |
||
| 355 | $row = self::fetchFromConds( $db, $conditions, $flags ); |
||
| 356 | if ( $row ) { |
||
| 357 | $rev = new Revision( $row ); |
||
| 358 | $rev->mWiki = $db->getWikiID(); |
||
| 359 | |||
| 360 | return $rev; |
||
| 361 | } |
||
| 362 | |||
| 363 | return null; |
||
| 364 | } |
||
| 365 | |||
| 366 | /** |
||
| 367 | * Return a wrapper for a series of database rows to |
||
| 368 | * fetch all of a given page's revisions in turn. |
||
| 369 | * Each row can be fed to the constructor to get objects. |
||
| 370 | * |
||
| 371 | * @param LinkTarget $title |
||
| 372 | * @return ResultWrapper |
||
| 373 | * @deprecated Since 1.28 |
||
| 374 | */ |
||
| 375 | public static function fetchRevision( LinkTarget $title ) { |
||
| 376 | $row = self::fetchFromConds( |
||
| 377 | wfGetDB( DB_REPLICA ), |
||
| 378 | [ |
||
| 379 | 'rev_id=page_latest', |
||
| 380 | 'page_namespace' => $title->getNamespace(), |
||
| 381 | 'page_title' => $title->getDBkey() |
||
| 382 | ] |
||
| 383 | ); |
||
| 384 | |||
| 385 | return new FakeResultWrapper( $row ? [ $row ] : [] ); |
||
| 386 | } |
||
| 387 | |||
| 388 | /** |
||
| 389 | * Given a set of conditions, return a ResultWrapper |
||
| 390 | * which will return matching database rows with the |
||
| 391 | * fields necessary to build Revision objects. |
||
| 392 | * |
||
| 393 | * @param IDatabase $db |
||
| 394 | * @param array $conditions |
||
| 395 | * @param int $flags (optional) |
||
| 396 | * @return stdClass |
||
| 397 | */ |
||
| 398 | private static function fetchFromConds( $db, $conditions, $flags = 0 ) { |
||
| 399 | $fields = array_merge( |
||
| 400 | self::selectFields(), |
||
| 401 | self::selectPageFields(), |
||
| 402 | self::selectUserFields() |
||
| 403 | ); |
||
| 404 | $options = []; |
||
| 405 | if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) { |
||
| 406 | $options[] = 'FOR UPDATE'; |
||
| 407 | } |
||
| 408 | return $db->selectRow( |
||
| 409 | [ 'revision', 'page', 'user' ], |
||
| 410 | $fields, |
||
| 411 | $conditions, |
||
| 412 | __METHOD__, |
||
| 413 | $options, |
||
| 414 | [ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ] |
||
| 415 | ); |
||
| 416 | } |
||
| 417 | |||
| 418 | /** |
||
| 419 | * Return the value of a select() JOIN conds array for the user table. |
||
| 420 | * This will get user table rows for logged-in users. |
||
| 421 | * @since 1.19 |
||
| 422 | * @return array |
||
| 423 | */ |
||
| 424 | public static function userJoinCond() { |
||
| 425 | return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ]; |
||
| 426 | } |
||
| 427 | |||
| 428 | /** |
||
| 429 | * Return the value of a select() page conds array for the page table. |
||
| 430 | * This will assure that the revision(s) are not orphaned from live pages. |
||
| 431 | * @since 1.19 |
||
| 432 | * @return array |
||
| 433 | */ |
||
| 434 | public static function pageJoinCond() { |
||
| 435 | return [ 'INNER JOIN', [ 'page_id = rev_page' ] ]; |
||
| 436 | } |
||
| 437 | |||
| 438 | /** |
||
| 439 | * Return the list of revision fields that should be selected to create |
||
| 440 | * a new revision. |
||
| 441 | * @return array |
||
| 442 | */ |
||
| 443 | public static function selectFields() { |
||
| 444 | global $wgContentHandlerUseDB; |
||
| 445 | |||
| 446 | $fields = [ |
||
| 447 | 'rev_id', |
||
| 448 | 'rev_page', |
||
| 449 | 'rev_text_id', |
||
| 450 | 'rev_timestamp', |
||
| 451 | 'rev_comment', |
||
| 452 | 'rev_user_text', |
||
| 453 | 'rev_user', |
||
| 454 | 'rev_minor_edit', |
||
| 455 | 'rev_deleted', |
||
| 456 | 'rev_len', |
||
| 457 | 'rev_parent_id', |
||
| 458 | 'rev_sha1', |
||
| 459 | ]; |
||
| 460 | |||
| 461 | if ( $wgContentHandlerUseDB ) { |
||
| 462 | $fields[] = 'rev_content_format'; |
||
| 463 | $fields[] = 'rev_content_model'; |
||
| 464 | } |
||
| 465 | |||
| 466 | return $fields; |
||
| 467 | } |
||
| 468 | |||
| 469 | /** |
||
| 470 | * Return the list of revision fields that should be selected to create |
||
| 471 | * a new revision from an archive row. |
||
| 472 | * @return array |
||
| 473 | */ |
||
| 474 | public static function selectArchiveFields() { |
||
| 475 | global $wgContentHandlerUseDB; |
||
| 476 | $fields = [ |
||
| 477 | 'ar_id', |
||
| 478 | 'ar_page_id', |
||
| 479 | 'ar_rev_id', |
||
| 480 | 'ar_text', |
||
| 481 | 'ar_text_id', |
||
| 482 | 'ar_timestamp', |
||
| 483 | 'ar_comment', |
||
| 484 | 'ar_user_text', |
||
| 485 | 'ar_user', |
||
| 486 | 'ar_minor_edit', |
||
| 487 | 'ar_deleted', |
||
| 488 | 'ar_len', |
||
| 489 | 'ar_parent_id', |
||
| 490 | 'ar_sha1', |
||
| 491 | ]; |
||
| 492 | |||
| 493 | if ( $wgContentHandlerUseDB ) { |
||
| 494 | $fields[] = 'ar_content_format'; |
||
| 495 | $fields[] = 'ar_content_model'; |
||
| 496 | } |
||
| 497 | return $fields; |
||
| 498 | } |
||
| 499 | |||
| 500 | /** |
||
| 501 | * Return the list of text fields that should be selected to read the |
||
| 502 | * revision text |
||
| 503 | * @return array |
||
| 504 | */ |
||
| 505 | public static function selectTextFields() { |
||
| 506 | return [ |
||
| 507 | 'old_text', |
||
| 508 | 'old_flags' |
||
| 509 | ]; |
||
| 510 | } |
||
| 511 | |||
| 512 | /** |
||
| 513 | * Return the list of page fields that should be selected from page table |
||
| 514 | * @return array |
||
| 515 | */ |
||
| 516 | public static function selectPageFields() { |
||
| 517 | return [ |
||
| 518 | 'page_namespace', |
||
| 519 | 'page_title', |
||
| 520 | 'page_id', |
||
| 521 | 'page_latest', |
||
| 522 | 'page_is_redirect', |
||
| 523 | 'page_len', |
||
| 524 | ]; |
||
| 525 | } |
||
| 526 | |||
| 527 | /** |
||
| 528 | * Return the list of user fields that should be selected from user table |
||
| 529 | * @return array |
||
| 530 | */ |
||
| 531 | public static function selectUserFields() { |
||
| 532 | return [ 'user_name' ]; |
||
| 533 | } |
||
| 534 | |||
| 535 | /** |
||
| 536 | * Do a batched query to get the parent revision lengths |
||
| 537 | * @param IDatabase $db |
||
| 538 | * @param array $revIds |
||
| 539 | * @return array |
||
| 540 | */ |
||
| 541 | public static function getParentLengths( $db, array $revIds ) { |
||
| 542 | $revLens = []; |
||
| 543 | if ( !$revIds ) { |
||
| 544 | return $revLens; // empty |
||
| 545 | } |
||
| 546 | $res = $db->select( 'revision', |
||
| 547 | [ 'rev_id', 'rev_len' ], |
||
| 548 | [ 'rev_id' => $revIds ], |
||
| 549 | __METHOD__ ); |
||
| 550 | foreach ( $res as $row ) { |
||
| 551 | $revLens[$row->rev_id] = $row->rev_len; |
||
| 552 | } |
||
| 553 | return $revLens; |
||
| 554 | } |
||
| 555 | |||
| 556 | /** |
||
| 557 | * Constructor |
||
| 558 | * |
||
| 559 | * @param object|array $row Either a database row or an array |
||
| 560 | * @throws MWException |
||
| 561 | * @access private |
||
| 562 | */ |
||
| 563 | function __construct( $row ) { |
||
| 564 | if ( is_object( $row ) ) { |
||
| 565 | $this->mId = intval( $row->rev_id ); |
||
| 566 | $this->mPage = intval( $row->rev_page ); |
||
| 567 | $this->mTextId = intval( $row->rev_text_id ); |
||
| 568 | $this->mComment = $row->rev_comment; |
||
| 569 | $this->mUser = intval( $row->rev_user ); |
||
| 570 | $this->mMinorEdit = intval( $row->rev_minor_edit ); |
||
| 571 | $this->mTimestamp = $row->rev_timestamp; |
||
| 572 | $this->mDeleted = intval( $row->rev_deleted ); |
||
| 573 | |||
| 574 | if ( !isset( $row->rev_parent_id ) ) { |
||
| 575 | $this->mParentId = null; |
||
| 576 | } else { |
||
| 577 | $this->mParentId = intval( $row->rev_parent_id ); |
||
| 578 | } |
||
| 579 | |||
| 580 | if ( !isset( $row->rev_len ) ) { |
||
| 581 | $this->mSize = null; |
||
| 582 | } else { |
||
| 583 | $this->mSize = intval( $row->rev_len ); |
||
| 584 | } |
||
| 585 | |||
| 586 | if ( !isset( $row->rev_sha1 ) ) { |
||
| 587 | $this->mSha1 = null; |
||
| 588 | } else { |
||
| 589 | $this->mSha1 = $row->rev_sha1; |
||
| 590 | } |
||
| 591 | |||
| 592 | if ( isset( $row->page_latest ) ) { |
||
| 593 | $this->mCurrent = ( $row->rev_id == $row->page_latest ); |
||
| 594 | $this->mTitle = Title::newFromRow( $row ); |
||
| 595 | } else { |
||
| 596 | $this->mCurrent = false; |
||
| 597 | $this->mTitle = null; |
||
| 598 | } |
||
| 599 | |||
| 600 | if ( !isset( $row->rev_content_model ) ) { |
||
| 601 | $this->mContentModel = null; # determine on demand if needed |
||
| 602 | } else { |
||
| 603 | $this->mContentModel = strval( $row->rev_content_model ); |
||
| 604 | } |
||
| 605 | |||
| 606 | if ( !isset( $row->rev_content_format ) ) { |
||
| 607 | $this->mContentFormat = null; # determine on demand if needed |
||
| 608 | } else { |
||
| 609 | $this->mContentFormat = strval( $row->rev_content_format ); |
||
| 610 | } |
||
| 611 | |||
| 612 | // Lazy extraction... |
||
| 613 | $this->mText = null; |
||
| 614 | if ( isset( $row->old_text ) ) { |
||
| 615 | $this->mTextRow = $row; |
||
| 616 | } else { |
||
| 617 | // 'text' table row entry will be lazy-loaded |
||
| 618 | $this->mTextRow = null; |
||
| 619 | } |
||
| 620 | |||
| 621 | // Use user_name for users and rev_user_text for IPs... |
||
| 622 | $this->mUserText = null; // lazy load if left null |
||
| 623 | if ( $this->mUser == 0 ) { |
||
| 624 | $this->mUserText = $row->rev_user_text; // IP user |
||
| 625 | } elseif ( isset( $row->user_name ) ) { |
||
| 626 | $this->mUserText = $row->user_name; // logged-in user |
||
| 627 | } |
||
| 628 | $this->mOrigUserText = $row->rev_user_text; |
||
| 629 | } elseif ( is_array( $row ) ) { |
||
| 630 | // Build a new revision to be saved... |
||
| 631 | global $wgUser; // ugh |
||
| 632 | |||
| 633 | # if we have a content object, use it to set the model and type |
||
| 634 | if ( !empty( $row['content'] ) ) { |
||
| 635 | // @todo when is that set? test with external store setup! check out insertOn() [dk] |
||
| 636 | if ( !empty( $row['text_id'] ) ) { |
||
| 637 | throw new MWException( "Text already stored in external store (id {$row['text_id']}), " . |
||
| 638 | "can't serialize content object" ); |
||
| 639 | } |
||
| 640 | |||
| 641 | $row['content_model'] = $row['content']->getModel(); |
||
| 642 | # note: mContentFormat is initializes later accordingly |
||
| 643 | # note: content is serialized later in this method! |
||
| 644 | # also set text to null? |
||
| 645 | } |
||
| 646 | |||
| 647 | $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null; |
||
| 648 | $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null; |
||
| 649 | $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null; |
||
| 650 | $this->mUserText = isset( $row['user_text'] ) |
||
| 651 | ? strval( $row['user_text'] ) : $wgUser->getName(); |
||
| 652 | $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId(); |
||
| 653 | $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0; |
||
| 654 | $this->mTimestamp = isset( $row['timestamp'] ) |
||
| 655 | ? strval( $row['timestamp'] ) : wfTimestampNow(); |
||
| 656 | $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0; |
||
| 657 | $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null; |
||
| 658 | $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null; |
||
| 659 | $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; |
||
| 660 | |||
| 661 | $this->mContentModel = isset( $row['content_model'] ) |
||
| 662 | ? strval( $row['content_model'] ) : null; |
||
| 663 | $this->mContentFormat = isset( $row['content_format'] ) |
||
| 664 | ? strval( $row['content_format'] ) : null; |
||
| 665 | |||
| 666 | // Enforce spacing trimming on supplied text |
||
| 667 | $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null; |
||
| 668 | $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; |
||
| 669 | $this->mTextRow = null; |
||
| 670 | |||
| 671 | $this->mTitle = isset( $row['title'] ) ? $row['title'] : null; |
||
| 672 | |||
| 673 | // if we have a Content object, override mText and mContentModel |
||
| 674 | if ( !empty( $row['content'] ) ) { |
||
| 675 | if ( !( $row['content'] instanceof Content ) ) { |
||
| 676 | throw new MWException( '`content` field must contain a Content object.' ); |
||
| 677 | } |
||
| 678 | |||
| 679 | $handler = $this->getContentHandler(); |
||
| 680 | $this->mContent = $row['content']; |
||
| 681 | |||
| 682 | $this->mContentModel = $this->mContent->getModel(); |
||
| 683 | $this->mContentHandler = null; |
||
| 684 | |||
| 685 | $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() ); |
||
| 686 | } elseif ( $this->mText !== null ) { |
||
| 687 | $handler = $this->getContentHandler(); |
||
| 688 | $this->mContent = $handler->unserializeContent( $this->mText ); |
||
| 689 | } |
||
| 690 | |||
| 691 | // If we have a Title object, make sure it is consistent with mPage. |
||
| 692 | if ( $this->mTitle && $this->mTitle->exists() ) { |
||
| 693 | if ( $this->mPage === null ) { |
||
| 694 | // if the page ID wasn't known, set it now |
||
| 695 | $this->mPage = $this->mTitle->getArticleID(); |
||
| 696 | } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) { |
||
| 697 | // Got different page IDs. This may be legit (e.g. during undeletion), |
||
| 698 | // but it seems worth mentioning it in the log. |
||
| 699 | wfDebug( "Page ID " . $this->mPage . " mismatches the ID " . |
||
| 700 | $this->mTitle->getArticleID() . " provided by the Title object." ); |
||
| 701 | } |
||
| 702 | } |
||
| 703 | |||
| 704 | $this->mCurrent = false; |
||
| 705 | |||
| 706 | // If we still have no length, see it we have the text to figure it out |
||
| 707 | if ( !$this->mSize && $this->mContent !== null ) { |
||
|
0 ignored issues
–
show
|
|||
| 708 | $this->mSize = $this->mContent->getSize(); |
||
| 709 | } |
||
| 710 | |||
| 711 | // Same for sha1 |
||
| 712 | if ( $this->mSha1 === null ) { |
||
| 713 | $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText ); |
||
| 714 | } |
||
| 715 | |||
| 716 | // force lazy init |
||
| 717 | $this->getContentModel(); |
||
| 718 | $this->getContentFormat(); |
||
| 719 | } else { |
||
| 720 | throw new MWException( 'Revision constructor passed invalid row format.' ); |
||
| 721 | } |
||
| 722 | $this->mUnpatrolled = null; |
||
| 723 | } |
||
| 724 | |||
| 725 | /** |
||
| 726 | * Get revision ID |
||
| 727 | * |
||
| 728 | * @return int|null |
||
| 729 | */ |
||
| 730 | public function getId() { |
||
| 731 | return $this->mId; |
||
| 732 | } |
||
| 733 | |||
| 734 | /** |
||
| 735 | * Set the revision ID |
||
| 736 | * |
||
| 737 | * This should only be used for proposed revisions that turn out to be null edits |
||
| 738 | * |
||
| 739 | * @since 1.19 |
||
| 740 | * @param int $id |
||
| 741 | */ |
||
| 742 | public function setId( $id ) { |
||
| 743 | $this->mId = (int)$id; |
||
| 744 | } |
||
| 745 | |||
| 746 | /** |
||
| 747 | * Set the user ID/name |
||
| 748 | * |
||
| 749 | * This should only be used for proposed revisions that turn out to be null edits |
||
| 750 | * |
||
| 751 | * @since 1.28 |
||
| 752 | * @param integer $id User ID |
||
| 753 | * @param string $name User name |
||
| 754 | */ |
||
| 755 | public function setUserIdAndName( $id, $name ) { |
||
| 756 | $this->mUser = (int)$id; |
||
| 757 | $this->mUserText = $name; |
||
| 758 | $this->mOrigUserText = $name; |
||
| 759 | } |
||
| 760 | |||
| 761 | /** |
||
| 762 | * Get text row ID |
||
| 763 | * |
||
| 764 | * @return int|null |
||
| 765 | */ |
||
| 766 | public function getTextId() { |
||
| 767 | return $this->mTextId; |
||
| 768 | } |
||
| 769 | |||
| 770 | /** |
||
| 771 | * Get parent revision ID (the original previous page revision) |
||
| 772 | * |
||
| 773 | * @return int|null |
||
| 774 | */ |
||
| 775 | public function getParentId() { |
||
| 776 | return $this->mParentId; |
||
| 777 | } |
||
| 778 | |||
| 779 | /** |
||
| 780 | * Returns the length of the text in this revision, or null if unknown. |
||
| 781 | * |
||
| 782 | * @return int|null |
||
| 783 | */ |
||
| 784 | public function getSize() { |
||
| 785 | return $this->mSize; |
||
| 786 | } |
||
| 787 | |||
| 788 | /** |
||
| 789 | * Returns the base36 sha1 of the text in this revision, or null if unknown. |
||
| 790 | * |
||
| 791 | * @return string|null |
||
| 792 | */ |
||
| 793 | public function getSha1() { |
||
| 794 | return $this->mSha1; |
||
| 795 | } |
||
| 796 | |||
| 797 | /** |
||
| 798 | * Returns the title of the page associated with this entry or null. |
||
| 799 | * |
||
| 800 | * Will do a query, when title is not set and id is given. |
||
| 801 | * |
||
| 802 | * @return Title|null |
||
| 803 | */ |
||
| 804 | public function getTitle() { |
||
| 805 | if ( $this->mTitle !== null ) { |
||
| 806 | return $this->mTitle; |
||
| 807 | } |
||
| 808 | // rev_id is defined as NOT NULL, but this revision may not yet have been inserted. |
||
| 809 | if ( $this->mId !== null ) { |
||
| 810 | $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki ); |
||
| 811 | $row = $dbr->selectRow( |
||
| 812 | [ 'page', 'revision' ], |
||
| 813 | self::selectPageFields(), |
||
| 814 | [ 'page_id=rev_page', 'rev_id' => $this->mId ], |
||
| 815 | __METHOD__ |
||
| 816 | ); |
||
| 817 | if ( $row ) { |
||
| 818 | // @TODO: better foreign title handling |
||
| 819 | $this->mTitle = Title::newFromRow( $row ); |
||
| 820 | } |
||
| 821 | } |
||
| 822 | |||
| 823 | if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) { |
||
| 824 | // Loading by ID is best, though not possible for foreign titles |
||
| 825 | if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) { |
||
| 826 | $this->mTitle = Title::newFromID( $this->mPage ); |
||
| 827 | } |
||
| 828 | } |
||
| 829 | |||
| 830 | return $this->mTitle; |
||
| 831 | } |
||
| 832 | |||
| 833 | /** |
||
| 834 | * Set the title of the revision |
||
| 835 | * |
||
| 836 | * @param Title $title |
||
| 837 | */ |
||
| 838 | public function setTitle( $title ) { |
||
| 839 | $this->mTitle = $title; |
||
| 840 | } |
||
| 841 | |||
| 842 | /** |
||
| 843 | * Get the page ID |
||
| 844 | * |
||
| 845 | * @return int|null |
||
| 846 | */ |
||
| 847 | public function getPage() { |
||
| 848 | return $this->mPage; |
||
| 849 | } |
||
| 850 | |||
| 851 | /** |
||
| 852 | * Fetch revision's user id if it's available to the specified audience. |
||
| 853 | * If the specified audience does not have access to it, zero will be |
||
| 854 | * returned. |
||
| 855 | * |
||
| 856 | * @param int $audience One of: |
||
| 857 | * Revision::FOR_PUBLIC to be displayed to all users |
||
| 858 | * Revision::FOR_THIS_USER to be displayed to the given user |
||
| 859 | * Revision::RAW get the ID regardless of permissions |
||
| 860 | * @param User $user User object to check for, only if FOR_THIS_USER is passed |
||
| 861 | * to the $audience parameter |
||
| 862 | * @return int |
||
| 863 | */ |
||
| 864 | View Code Duplication | public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) { |
|
| 865 | if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { |
||
| 866 | return 0; |
||
| 867 | } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { |
||
| 868 | return 0; |
||
| 869 | } else { |
||
| 870 | return $this->mUser; |
||
| 871 | } |
||
| 872 | } |
||
| 873 | |||
| 874 | /** |
||
| 875 | * Fetch revision's user id without regard for the current user's permissions |
||
| 876 | * |
||
| 877 | * @return string |
||
| 878 | * @deprecated since 1.25, use getUser( Revision::RAW ) |
||
| 879 | */ |
||
| 880 | public function getRawUser() { |
||
| 881 | wfDeprecated( __METHOD__, '1.25' ); |
||
| 882 | return $this->getUser( self::RAW ); |
||
| 883 | } |
||
| 884 | |||
| 885 | /** |
||
| 886 | * Fetch revision's username if it's available to the specified audience. |
||
| 887 | * If the specified audience does not have access to the username, an |
||
| 888 | * empty string will be returned. |
||
| 889 | * |
||
| 890 | * @param int $audience One of: |
||
| 891 | * Revision::FOR_PUBLIC to be displayed to all users |
||
| 892 | * Revision::FOR_THIS_USER to be displayed to the given user |
||
| 893 | * Revision::RAW get the text regardless of permissions |
||
| 894 | * @param User $user User object to check for, only if FOR_THIS_USER is passed |
||
| 895 | * to the $audience parameter |
||
| 896 | * @return string |
||
| 897 | */ |
||
| 898 | public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { |
||
| 899 | $this->loadMutableFields(); |
||
| 900 | |||
| 901 | if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { |
||
| 902 | return ''; |
||
| 903 | } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { |
||
| 904 | return ''; |
||
| 905 | } else { |
||
| 906 | if ( $this->mUserText === null ) { |
||
| 907 | $this->mUserText = User::whoIs( $this->mUser ); // load on demand |
||
| 908 | if ( $this->mUserText === false ) { |
||
| 909 | # This shouldn't happen, but it can if the wiki was recovered |
||
| 910 | # via importing revs and there is no user table entry yet. |
||
| 911 | $this->mUserText = $this->mOrigUserText; |
||
| 912 | } |
||
| 913 | } |
||
| 914 | return $this->mUserText; |
||
| 915 | } |
||
| 916 | } |
||
| 917 | |||
| 918 | /** |
||
| 919 | * Fetch revision's username without regard for view restrictions |
||
| 920 | * |
||
| 921 | * @return string |
||
| 922 | * @deprecated since 1.25, use getUserText( Revision::RAW ) |
||
| 923 | */ |
||
| 924 | public function getRawUserText() { |
||
| 925 | wfDeprecated( __METHOD__, '1.25' ); |
||
| 926 | return $this->getUserText( self::RAW ); |
||
| 927 | } |
||
| 928 | |||
| 929 | /** |
||
| 930 | * Fetch revision comment if it's available to the specified audience. |
||
| 931 | * If the specified audience does not have access to the comment, an |
||
| 932 | * empty string will be returned. |
||
| 933 | * |
||
| 934 | * @param int $audience One of: |
||
| 935 | * Revision::FOR_PUBLIC to be displayed to all users |
||
| 936 | * Revision::FOR_THIS_USER to be displayed to the given user |
||
| 937 | * Revision::RAW get the text regardless of permissions |
||
| 938 | * @param User $user User object to check for, only if FOR_THIS_USER is passed |
||
| 939 | * to the $audience parameter |
||
| 940 | * @return string |
||
| 941 | */ |
||
| 942 | View Code Duplication | function getComment( $audience = self::FOR_PUBLIC, User $user = null ) { |
|
| 943 | if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { |
||
| 944 | return ''; |
||
| 945 | } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) { |
||
| 946 | return ''; |
||
| 947 | } else { |
||
| 948 | return $this->mComment; |
||
| 949 | } |
||
| 950 | } |
||
| 951 | |||
| 952 | /** |
||
| 953 | * Fetch revision comment without regard for the current user's permissions |
||
| 954 | * |
||
| 955 | * @return string |
||
| 956 | * @deprecated since 1.25, use getComment( Revision::RAW ) |
||
| 957 | */ |
||
| 958 | public function getRawComment() { |
||
| 959 | wfDeprecated( __METHOD__, '1.25' ); |
||
| 960 | return $this->getComment( self::RAW ); |
||
| 961 | } |
||
| 962 | |||
| 963 | /** |
||
| 964 | * @return bool |
||
| 965 | */ |
||
| 966 | public function isMinor() { |
||
| 967 | return (bool)$this->mMinorEdit; |
||
| 968 | } |
||
| 969 | |||
| 970 | /** |
||
| 971 | * @return int Rcid of the unpatrolled row, zero if there isn't one |
||
| 972 | */ |
||
| 973 | public function isUnpatrolled() { |
||
| 974 | if ( $this->mUnpatrolled !== null ) { |
||
| 975 | return $this->mUnpatrolled; |
||
| 976 | } |
||
| 977 | $rc = $this->getRecentChange(); |
||
| 978 | if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) { |
||
| 979 | $this->mUnpatrolled = $rc->getAttribute( 'rc_id' ); |
||
| 980 | } else { |
||
| 981 | $this->mUnpatrolled = 0; |
||
| 982 | } |
||
| 983 | return $this->mUnpatrolled; |
||
| 984 | } |
||
| 985 | |||
| 986 | /** |
||
| 987 | * Get the RC object belonging to the current revision, if there's one |
||
| 988 | * |
||
| 989 | * @param int $flags (optional) $flags include: |
||
| 990 | * Revision::READ_LATEST : Select the data from the master |
||
| 991 | * |
||
| 992 | * @since 1.22 |
||
| 993 | * @return RecentChange|null |
||
| 994 | */ |
||
| 995 | public function getRecentChange( $flags = 0 ) { |
||
| 996 | $dbr = wfGetDB( DB_REPLICA ); |
||
| 997 | |||
| 998 | list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags ); |
||
| 999 | |||
| 1000 | return RecentChange::newFromConds( |
||
| 1001 | [ |
||
| 1002 | 'rc_user_text' => $this->getUserText( Revision::RAW ), |
||
| 1003 | 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ), |
||
| 1004 | 'rc_this_oldid' => $this->getId() |
||
| 1005 | ], |
||
| 1006 | __METHOD__, |
||
| 1007 | $dbType |
||
| 1008 | ); |
||
| 1009 | } |
||
| 1010 | |||
| 1011 | /** |
||
| 1012 | * @param int $field One of DELETED_* bitfield constants |
||
| 1013 | * |
||
| 1014 | * @return bool |
||
| 1015 | */ |
||
| 1016 | public function isDeleted( $field ) { |
||
| 1017 | if ( $this->isCurrent() && $field === self::DELETED_TEXT ) { |
||
| 1018 | // Current revisions of pages cannot have the content hidden. Skipping this |
||
| 1019 | // check is very useful for Parser as it fetches templates using newKnownCurrent(). |
||
| 1020 | // Calling getVisibility() in that case triggers a verification database query. |
||
| 1021 | return false; // no need to check |
||
| 1022 | } |
||
| 1023 | |||
| 1024 | return ( $this->getVisibility() & $field ) == $field; |
||
| 1025 | } |
||
| 1026 | |||
| 1027 | /** |
||
| 1028 | * Get the deletion bitfield of the revision |
||
| 1029 | * |
||
| 1030 | * @return int |
||
| 1031 | */ |
||
| 1032 | public function getVisibility() { |
||
| 1033 | $this->loadMutableFields(); |
||
| 1034 | |||
| 1035 | return (int)$this->mDeleted; |
||
| 1036 | } |
||
| 1037 | |||
| 1038 | /** |
||
| 1039 | * Fetch revision text if it's available to the specified audience. |
||
| 1040 | * If the specified audience does not have the ability to view this |
||
| 1041 | * revision, an empty string will be returned. |
||
| 1042 | * |
||
| 1043 | * @param int $audience One of: |
||
| 1044 | * Revision::FOR_PUBLIC to be displayed to all users |
||
| 1045 | * Revision::FOR_THIS_USER to be displayed to the given user |
||
| 1046 | * Revision::RAW get the text regardless of permissions |
||
| 1047 | * @param User $user User object to check for, only if FOR_THIS_USER is passed |
||
| 1048 | * to the $audience parameter |
||
| 1049 | * |
||
| 1050 | * @deprecated since 1.21, use getContent() instead |
||
| 1051 | * @return string |
||
| 1052 | */ |
||
| 1053 | public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { |
||
| 1054 | wfDeprecated( __METHOD__, '1.21' ); |
||
| 1055 | |||
| 1056 | $content = $this->getContent( $audience, $user ); |
||
| 1057 | return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable |
||
| 1058 | } |
||
| 1059 | |||
| 1060 | /** |
||
| 1061 | * Fetch revision content if it's available to the specified audience. |
||
| 1062 | * If the specified audience does not have the ability to view this |
||
| 1063 | * revision, null will be returned. |
||
| 1064 | * |
||
| 1065 | * @param int $audience One of: |
||
| 1066 | * Revision::FOR_PUBLIC to be displayed to all users |
||
| 1067 | * Revision::FOR_THIS_USER to be displayed to $wgUser |
||
| 1068 | * Revision::RAW get the text regardless of permissions |
||
| 1069 | * @param User $user User object to check for, only if FOR_THIS_USER is passed |
||
| 1070 | * to the $audience parameter |
||
| 1071 | * @since 1.21 |
||
| 1072 | * @return Content|null |
||
| 1073 | */ |
||
| 1074 | public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { |
||
| 1075 | if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { |
||
| 1076 | return null; |
||
| 1077 | } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { |
||
| 1078 | return null; |
||
| 1079 | } else { |
||
| 1080 | return $this->getContentInternal(); |
||
| 1081 | } |
||
| 1082 | } |
||
| 1083 | |||
| 1084 | /** |
||
| 1085 | * Get original serialized data (without checking view restrictions) |
||
| 1086 | * |
||
| 1087 | * @since 1.21 |
||
| 1088 | * @return string |
||
| 1089 | */ |
||
| 1090 | public function getSerializedData() { |
||
| 1091 | if ( $this->mText === null ) { |
||
| 1092 | // Revision is immutable. Load on demand. |
||
| 1093 | $this->mText = $this->loadText(); |
||
| 1094 | } |
||
| 1095 | |||
| 1096 | return $this->mText; |
||
| 1097 | } |
||
| 1098 | |||
| 1099 | /** |
||
| 1100 | * Gets the content object for the revision (or null on failure). |
||
| 1101 | * |
||
| 1102 | * Note that for mutable Content objects, each call to this method will return a |
||
| 1103 | * fresh clone. |
||
| 1104 | * |
||
| 1105 | * @since 1.21 |
||
| 1106 | * @return Content|null The Revision's content, or null on failure. |
||
| 1107 | */ |
||
| 1108 | protected function getContentInternal() { |
||
| 1109 | if ( $this->mContent === null ) { |
||
| 1110 | $text = $this->getSerializedData(); |
||
| 1111 | |||
| 1112 | if ( $text !== null && $text !== false ) { |
||
| 1113 | // Unserialize content |
||
| 1114 | $handler = $this->getContentHandler(); |
||
| 1115 | $format = $this->getContentFormat(); |
||
| 1116 | |||
| 1117 | $this->mContent = $handler->unserializeContent( $text, $format ); |
||
| 1118 | } |
||
| 1119 | } |
||
| 1120 | |||
| 1121 | // NOTE: copy() will return $this for immutable content objects |
||
| 1122 | return $this->mContent ? $this->mContent->copy() : null; |
||
| 1123 | } |
||
| 1124 | |||
| 1125 | /** |
||
| 1126 | * Returns the content model for this revision. |
||
| 1127 | * |
||
| 1128 | * If no content model was stored in the database, the default content model for the title is |
||
| 1129 | * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT |
||
| 1130 | * is used as a last resort. |
||
| 1131 | * |
||
| 1132 | * @return string The content model id associated with this revision, |
||
| 1133 | * see the CONTENT_MODEL_XXX constants. |
||
| 1134 | **/ |
||
| 1135 | public function getContentModel() { |
||
| 1136 | if ( !$this->mContentModel ) { |
||
| 1137 | $title = $this->getTitle(); |
||
| 1138 | if ( $title ) { |
||
| 1139 | $this->mContentModel = ContentHandler::getDefaultModelFor( $title ); |
||
| 1140 | } else { |
||
| 1141 | $this->mContentModel = CONTENT_MODEL_WIKITEXT; |
||
| 1142 | } |
||
| 1143 | |||
| 1144 | assert( !empty( $this->mContentModel ) ); |
||
| 1145 | } |
||
| 1146 | |||
| 1147 | return $this->mContentModel; |
||
| 1148 | } |
||
| 1149 | |||
| 1150 | /** |
||
| 1151 | * Returns the content format for this revision. |
||
| 1152 | * |
||
| 1153 | * If no content format was stored in the database, the default format for this |
||
| 1154 | * revision's content model is returned. |
||
| 1155 | * |
||
| 1156 | * @return string The content format id associated with this revision, |
||
| 1157 | * see the CONTENT_FORMAT_XXX constants. |
||
| 1158 | **/ |
||
| 1159 | public function getContentFormat() { |
||
| 1160 | if ( !$this->mContentFormat ) { |
||
| 1161 | $handler = $this->getContentHandler(); |
||
| 1162 | $this->mContentFormat = $handler->getDefaultFormat(); |
||
| 1163 | |||
| 1164 | assert( !empty( $this->mContentFormat ) ); |
||
| 1165 | } |
||
| 1166 | |||
| 1167 | return $this->mContentFormat; |
||
| 1168 | } |
||
| 1169 | |||
| 1170 | /** |
||
| 1171 | * Returns the content handler appropriate for this revision's content model. |
||
| 1172 | * |
||
| 1173 | * @throws MWException |
||
| 1174 | * @return ContentHandler |
||
| 1175 | */ |
||
| 1176 | public function getContentHandler() { |
||
| 1177 | if ( !$this->mContentHandler ) { |
||
| 1178 | $model = $this->getContentModel(); |
||
| 1179 | $this->mContentHandler = ContentHandler::getForModelID( $model ); |
||
| 1180 | |||
| 1181 | $format = $this->getContentFormat(); |
||
| 1182 | |||
| 1183 | if ( !$this->mContentHandler->isSupportedFormat( $format ) ) { |
||
| 1184 | throw new MWException( "Oops, the content format $format is not supported for " |
||
| 1185 | . "this content model, $model" ); |
||
| 1186 | } |
||
| 1187 | } |
||
| 1188 | |||
| 1189 | return $this->mContentHandler; |
||
| 1190 | } |
||
| 1191 | |||
| 1192 | /** |
||
| 1193 | * @return string |
||
| 1194 | */ |
||
| 1195 | public function getTimestamp() { |
||
| 1196 | return wfTimestamp( TS_MW, $this->mTimestamp ); |
||
| 1197 | } |
||
| 1198 | |||
| 1199 | /** |
||
| 1200 | * @return bool |
||
| 1201 | */ |
||
| 1202 | public function isCurrent() { |
||
| 1203 | return $this->mCurrent; |
||
| 1204 | } |
||
| 1205 | |||
| 1206 | /** |
||
| 1207 | * Get previous revision for this title |
||
| 1208 | * |
||
| 1209 | * @return Revision|null |
||
| 1210 | */ |
||
| 1211 | View Code Duplication | public function getPrevious() { |
|
| 1212 | if ( $this->getTitle() ) { |
||
| 1213 | $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); |
||
| 1214 | if ( $prev ) { |
||
| 1215 | return self::newFromTitle( $this->getTitle(), $prev ); |
||
| 1216 | } |
||
| 1217 | } |
||
| 1218 | return null; |
||
| 1219 | } |
||
| 1220 | |||
| 1221 | /** |
||
| 1222 | * Get next revision for this title |
||
| 1223 | * |
||
| 1224 | * @return Revision|null |
||
| 1225 | */ |
||
| 1226 | View Code Duplication | public function getNext() { |
|
| 1227 | if ( $this->getTitle() ) { |
||
| 1228 | $next = $this->getTitle()->getNextRevisionID( $this->getId() ); |
||
| 1229 | if ( $next ) { |
||
| 1230 | return self::newFromTitle( $this->getTitle(), $next ); |
||
| 1231 | } |
||
| 1232 | } |
||
| 1233 | return null; |
||
| 1234 | } |
||
| 1235 | |||
| 1236 | /** |
||
| 1237 | * Get previous revision Id for this page_id |
||
| 1238 | * This is used to populate rev_parent_id on save |
||
| 1239 | * |
||
| 1240 | * @param IDatabase $db |
||
| 1241 | * @return int |
||
| 1242 | */ |
||
| 1243 | private function getPreviousRevisionId( $db ) { |
||
| 1244 | if ( $this->mPage === null ) { |
||
| 1245 | return 0; |
||
| 1246 | } |
||
| 1247 | # Use page_latest if ID is not given |
||
| 1248 | if ( !$this->mId ) { |
||
| 1249 | $prevId = $db->selectField( 'page', 'page_latest', |
||
| 1250 | [ 'page_id' => $this->mPage ], |
||
| 1251 | __METHOD__ ); |
||
| 1252 | } else { |
||
| 1253 | $prevId = $db->selectField( 'revision', 'rev_id', |
||
| 1254 | [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ], |
||
| 1255 | __METHOD__, |
||
| 1256 | [ 'ORDER BY' => 'rev_id DESC' ] ); |
||
| 1257 | } |
||
| 1258 | return intval( $prevId ); |
||
| 1259 | } |
||
| 1260 | |||
| 1261 | /** |
||
| 1262 | * Get revision text associated with an old or archive row |
||
| 1263 | * $row is usually an object from wfFetchRow(), both the flags and the text |
||
| 1264 | * field must be included. |
||
| 1265 | * |
||
| 1266 | * @param stdClass $row The text data |
||
| 1267 | * @param string $prefix Table prefix (default 'old_') |
||
| 1268 | * @param string|bool $wiki The name of the wiki to load the revision text from |
||
| 1269 | * (same as the the wiki $row was loaded from) or false to indicate the local |
||
| 1270 | * wiki (this is the default). Otherwise, it must be a symbolic wiki database |
||
| 1271 | * identifier as understood by the LoadBalancer class. |
||
| 1272 | * @return string Text the text requested or false on failure |
||
| 1273 | */ |
||
| 1274 | public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) { |
||
| 1275 | |||
| 1276 | # Get data |
||
| 1277 | $textField = $prefix . 'text'; |
||
| 1278 | $flagsField = $prefix . 'flags'; |
||
| 1279 | |||
| 1280 | if ( isset( $row->$flagsField ) ) { |
||
| 1281 | $flags = explode( ',', $row->$flagsField ); |
||
| 1282 | } else { |
||
| 1283 | $flags = []; |
||
| 1284 | } |
||
| 1285 | |||
| 1286 | if ( isset( $row->$textField ) ) { |
||
| 1287 | $text = $row->$textField; |
||
| 1288 | } else { |
||
| 1289 | return false; |
||
| 1290 | } |
||
| 1291 | |||
| 1292 | # Use external methods for external objects, text in table is URL-only then |
||
| 1293 | if ( in_array( 'external', $flags ) ) { |
||
| 1294 | $url = $text; |
||
| 1295 | $parts = explode( '://', $url, 2 ); |
||
| 1296 | if ( count( $parts ) == 1 || $parts[1] == '' ) { |
||
| 1297 | return false; |
||
| 1298 | } |
||
| 1299 | $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] ); |
||
| 1300 | } |
||
| 1301 | |||
| 1302 | // If the text was fetched without an error, convert it |
||
| 1303 | if ( $text !== false ) { |
||
| 1304 | $text = self::decompressRevisionText( $text, $flags ); |
||
| 1305 | } |
||
| 1306 | return $text; |
||
| 1307 | } |
||
| 1308 | |||
| 1309 | /** |
||
| 1310 | * If $wgCompressRevisions is enabled, we will compress data. |
||
| 1311 | * The input string is modified in place. |
||
| 1312 | * Return value is the flags field: contains 'gzip' if the |
||
| 1313 | * data is compressed, and 'utf-8' if we're saving in UTF-8 |
||
| 1314 | * mode. |
||
| 1315 | * |
||
| 1316 | * @param mixed $text Reference to a text |
||
| 1317 | * @return string |
||
| 1318 | */ |
||
| 1319 | public static function compressRevisionText( &$text ) { |
||
| 1320 | global $wgCompressRevisions; |
||
| 1321 | $flags = []; |
||
| 1322 | |||
| 1323 | # Revisions not marked this way will be converted |
||
| 1324 | # on load if $wgLegacyCharset is set in the future. |
||
| 1325 | $flags[] = 'utf-8'; |
||
| 1326 | |||
| 1327 | if ( $wgCompressRevisions ) { |
||
| 1328 | if ( function_exists( 'gzdeflate' ) ) { |
||
| 1329 | $deflated = gzdeflate( $text ); |
||
| 1330 | |||
| 1331 | if ( $deflated === false ) { |
||
| 1332 | wfLogWarning( __METHOD__ . ': gzdeflate() failed' ); |
||
| 1333 | } else { |
||
| 1334 | $text = $deflated; |
||
| 1335 | $flags[] = 'gzip'; |
||
| 1336 | } |
||
| 1337 | } else { |
||
| 1338 | wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" ); |
||
| 1339 | } |
||
| 1340 | } |
||
| 1341 | return implode( ',', $flags ); |
||
| 1342 | } |
||
| 1343 | |||
| 1344 | /** |
||
| 1345 | * Re-converts revision text according to it's flags. |
||
| 1346 | * |
||
| 1347 | * @param mixed $text Reference to a text |
||
| 1348 | * @param array $flags Compression flags |
||
| 1349 | * @return string|bool Decompressed text, or false on failure |
||
| 1350 | */ |
||
| 1351 | public static function decompressRevisionText( $text, $flags ) { |
||
| 1352 | if ( in_array( 'gzip', $flags ) ) { |
||
| 1353 | # Deal with optional compression of archived pages. |
||
| 1354 | # This can be done periodically via maintenance/compressOld.php, and |
||
| 1355 | # as pages are saved if $wgCompressRevisions is set. |
||
| 1356 | $text = gzinflate( $text ); |
||
| 1357 | |||
| 1358 | if ( $text === false ) { |
||
| 1359 | wfLogWarning( __METHOD__ . ': gzinflate() failed' ); |
||
| 1360 | return false; |
||
| 1361 | } |
||
| 1362 | } |
||
| 1363 | |||
| 1364 | if ( in_array( 'object', $flags ) ) { |
||
| 1365 | # Generic compressed storage |
||
| 1366 | $obj = unserialize( $text ); |
||
| 1367 | if ( !is_object( $obj ) ) { |
||
| 1368 | // Invalid object |
||
| 1369 | return false; |
||
| 1370 | } |
||
| 1371 | $text = $obj->getText(); |
||
| 1372 | } |
||
| 1373 | |||
| 1374 | global $wgLegacyEncoding; |
||
| 1375 | if ( $text !== false && $wgLegacyEncoding |
||
| 1376 | && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) |
||
| 1377 | ) { |
||
| 1378 | # Old revisions kept around in a legacy encoding? |
||
| 1379 | # Upconvert on demand. |
||
| 1380 | # ("utf8" checked for compatibility with some broken |
||
| 1381 | # conversion scripts 2008-12-30) |
||
| 1382 | global $wgContLang; |
||
| 1383 | $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text ); |
||
| 1384 | } |
||
| 1385 | |||
| 1386 | return $text; |
||
| 1387 | } |
||
| 1388 | |||
| 1389 | /** |
||
| 1390 | * Insert a new revision into the database, returning the new revision ID |
||
| 1391 | * number on success and dies horribly on failure. |
||
| 1392 | * |
||
| 1393 | * @param IDatabase $dbw (master connection) |
||
| 1394 | * @throws MWException |
||
| 1395 | * @return int |
||
| 1396 | */ |
||
| 1397 | public function insertOn( $dbw ) { |
||
| 1398 | global $wgDefaultExternalStore, $wgContentHandlerUseDB; |
||
| 1399 | |||
| 1400 | // We're inserting a new revision, so we have to use master anyway. |
||
| 1401 | // If it's a null revision, it may have references to rows that |
||
| 1402 | // are not in the replica yet (the text row). |
||
| 1403 | $this->mQueryFlags |= self::READ_LATEST; |
||
| 1404 | |||
| 1405 | // Not allowed to have rev_page equal to 0, false, etc. |
||
| 1406 | View Code Duplication | if ( !$this->mPage ) { |
|
| 1407 | $title = $this->getTitle(); |
||
| 1408 | if ( $title instanceof Title ) { |
||
| 1409 | $titleText = ' for page ' . $title->getPrefixedText(); |
||
| 1410 | } else { |
||
| 1411 | $titleText = ''; |
||
| 1412 | } |
||
| 1413 | throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" ); |
||
| 1414 | } |
||
| 1415 | |||
| 1416 | $this->checkContentModel(); |
||
| 1417 | |||
| 1418 | $data = $this->mText; |
||
| 1419 | $flags = self::compressRevisionText( $data ); |
||
| 1420 | |||
| 1421 | # Write to external storage if required |
||
| 1422 | if ( $wgDefaultExternalStore ) { |
||
| 1423 | // Store and get the URL |
||
| 1424 | $data = ExternalStore::insertToDefault( $data ); |
||
| 1425 | if ( !$data ) { |
||
| 1426 | throw new MWException( "Unable to store text to external storage" ); |
||
| 1427 | } |
||
| 1428 | if ( $flags ) { |
||
| 1429 | $flags .= ','; |
||
| 1430 | } |
||
| 1431 | $flags .= 'external'; |
||
| 1432 | } |
||
| 1433 | |||
| 1434 | # Record the text (or external storage URL) to the text table |
||
| 1435 | if ( $this->mTextId === null ) { |
||
| 1436 | $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' ); |
||
| 1437 | $dbw->insert( 'text', |
||
| 1438 | [ |
||
| 1439 | 'old_id' => $old_id, |
||
| 1440 | 'old_text' => $data, |
||
| 1441 | 'old_flags' => $flags, |
||
| 1442 | ], __METHOD__ |
||
| 1443 | ); |
||
| 1444 | $this->mTextId = $dbw->insertId(); |
||
| 1445 | } |
||
| 1446 | |||
| 1447 | if ( $this->mComment === null ) { |
||
| 1448 | $this->mComment = ""; |
||
| 1449 | } |
||
| 1450 | |||
| 1451 | # Record the edit in revisions |
||
| 1452 | $rev_id = $this->mId !== null |
||
| 1453 | ? $this->mId |
||
| 1454 | : $dbw->nextSequenceValue( 'revision_rev_id_seq' ); |
||
| 1455 | $row = [ |
||
| 1456 | 'rev_id' => $rev_id, |
||
| 1457 | 'rev_page' => $this->mPage, |
||
| 1458 | 'rev_text_id' => $this->mTextId, |
||
| 1459 | 'rev_comment' => $this->mComment, |
||
| 1460 | 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0, |
||
| 1461 | 'rev_user' => $this->mUser, |
||
| 1462 | 'rev_user_text' => $this->mUserText, |
||
| 1463 | 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), |
||
| 1464 | 'rev_deleted' => $this->mDeleted, |
||
| 1465 | 'rev_len' => $this->mSize, |
||
| 1466 | 'rev_parent_id' => $this->mParentId === null |
||
| 1467 | ? $this->getPreviousRevisionId( $dbw ) |
||
| 1468 | : $this->mParentId, |
||
| 1469 | 'rev_sha1' => $this->mSha1 === null |
||
| 1470 | ? Revision::base36Sha1( $this->mText ) |
||
| 1471 | : $this->mSha1, |
||
| 1472 | ]; |
||
| 1473 | |||
| 1474 | if ( $wgContentHandlerUseDB ) { |
||
| 1475 | // NOTE: Store null for the default model and format, to save space. |
||
| 1476 | // XXX: Makes the DB sensitive to changed defaults. |
||
| 1477 | // Make this behavior optional? Only in miser mode? |
||
| 1478 | |||
| 1479 | $model = $this->getContentModel(); |
||
| 1480 | $format = $this->getContentFormat(); |
||
| 1481 | |||
| 1482 | $title = $this->getTitle(); |
||
| 1483 | |||
| 1484 | if ( $title === null ) { |
||
| 1485 | throw new MWException( "Insufficient information to determine the title of the " |
||
| 1486 | . "revision's page!" ); |
||
| 1487 | } |
||
| 1488 | |||
| 1489 | $defaultModel = ContentHandler::getDefaultModelFor( $title ); |
||
| 1490 | $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat(); |
||
| 1491 | |||
| 1492 | $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model; |
||
| 1493 | $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format; |
||
| 1494 | } |
||
| 1495 | |||
| 1496 | $dbw->insert( 'revision', $row, __METHOD__ ); |
||
| 1497 | |||
| 1498 | $this->mId = $rev_id !== null ? $rev_id : $dbw->insertId(); |
||
| 1499 | |||
| 1500 | // Assertion to try to catch T92046 |
||
| 1501 | if ( (int)$this->mId === 0 ) { |
||
| 1502 | throw new UnexpectedValueException( |
||
| 1503 | 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' . |
||
| 1504 | var_export( $row, 1 ) |
||
| 1505 | ); |
||
| 1506 | } |
||
| 1507 | |||
| 1508 | Hooks::run( 'RevisionInsertComplete', [ &$this, $data, $flags ] ); |
||
| 1509 | |||
| 1510 | return $this->mId; |
||
| 1511 | } |
||
| 1512 | |||
| 1513 | protected function checkContentModel() { |
||
| 1514 | global $wgContentHandlerUseDB; |
||
| 1515 | |||
| 1516 | // Note: may return null for revisions that have not yet been inserted |
||
| 1517 | $title = $this->getTitle(); |
||
| 1518 | |||
| 1519 | $model = $this->getContentModel(); |
||
| 1520 | $format = $this->getContentFormat(); |
||
| 1521 | $handler = $this->getContentHandler(); |
||
| 1522 | |||
| 1523 | if ( !$handler->isSupportedFormat( $format ) ) { |
||
| 1524 | $t = $title->getPrefixedDBkey(); |
||
| 1525 | |||
| 1526 | throw new MWException( "Can't use format $format with content model $model on $t" ); |
||
| 1527 | } |
||
| 1528 | |||
| 1529 | if ( !$wgContentHandlerUseDB && $title ) { |
||
| 1530 | // if $wgContentHandlerUseDB is not set, |
||
| 1531 | // all revisions must use the default content model and format. |
||
| 1532 | |||
| 1533 | $defaultModel = ContentHandler::getDefaultModelFor( $title ); |
||
| 1534 | $defaultHandler = ContentHandler::getForModelID( $defaultModel ); |
||
| 1535 | $defaultFormat = $defaultHandler->getDefaultFormat(); |
||
| 1536 | |||
| 1537 | if ( $this->getContentModel() != $defaultModel ) { |
||
| 1538 | $t = $title->getPrefixedDBkey(); |
||
| 1539 | |||
| 1540 | throw new MWException( "Can't save non-default content model with " |
||
| 1541 | . "\$wgContentHandlerUseDB disabled: model is $model, " |
||
| 1542 | . "default for $t is $defaultModel" ); |
||
| 1543 | } |
||
| 1544 | |||
| 1545 | if ( $this->getContentFormat() != $defaultFormat ) { |
||
| 1546 | $t = $title->getPrefixedDBkey(); |
||
| 1547 | |||
| 1548 | throw new MWException( "Can't use non-default content format with " |
||
| 1549 | . "\$wgContentHandlerUseDB disabled: format is $format, " |
||
| 1550 | . "default for $t is $defaultFormat" ); |
||
| 1551 | } |
||
| 1552 | } |
||
| 1553 | |||
| 1554 | $content = $this->getContent( Revision::RAW ); |
||
| 1555 | $prefixedDBkey = $title->getPrefixedDBkey(); |
||
| 1556 | $revId = $this->mId; |
||
| 1557 | |||
| 1558 | if ( !$content ) { |
||
| 1559 | throw new MWException( |
||
| 1560 | "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!" |
||
| 1561 | ); |
||
| 1562 | } |
||
| 1563 | if ( !$content->isValid() ) { |
||
| 1564 | throw new MWException( |
||
| 1565 | "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model" |
||
| 1566 | ); |
||
| 1567 | } |
||
| 1568 | } |
||
| 1569 | |||
| 1570 | /** |
||
| 1571 | * Get the base 36 SHA-1 value for a string of text |
||
| 1572 | * @param string $text |
||
| 1573 | * @return string |
||
| 1574 | */ |
||
| 1575 | public static function base36Sha1( $text ) { |
||
| 1576 | return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 ); |
||
| 1577 | } |
||
| 1578 | |||
| 1579 | /** |
||
| 1580 | * Lazy-load the revision's text. |
||
| 1581 | * Currently hardcoded to the 'text' table storage engine. |
||
| 1582 | * |
||
| 1583 | * @return string|bool The revision's text, or false on failure |
||
| 1584 | */ |
||
| 1585 | private function loadText() { |
||
| 1586 | global $wgRevisionCacheExpiry; |
||
| 1587 | |||
| 1588 | $cache = ObjectCache::getMainWANInstance(); |
||
| 1589 | if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) { |
||
| 1590 | // Do not cache RDBMs blobs in...the RDBMs store |
||
| 1591 | $ttl = $cache::TTL_UNCACHEABLE; |
||
| 1592 | } else { |
||
| 1593 | $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE; |
||
| 1594 | } |
||
| 1595 | |||
| 1596 | // No negative caching; negative hits on text rows may be due to corrupted replica DBs |
||
| 1597 | return $cache->getWithSetCallback( |
||
| 1598 | $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ), |
||
| 1599 | $ttl, |
||
| 1600 | function () { |
||
| 1601 | return $this->fetchText(); |
||
| 1602 | }, |
||
| 1603 | [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ] |
||
| 1604 | ); |
||
| 1605 | } |
||
| 1606 | |||
| 1607 | private function fetchText() { |
||
| 1608 | $textId = $this->getTextId(); |
||
| 1609 | |||
| 1610 | // If we kept data for lazy extraction, use it now... |
||
| 1611 | if ( $this->mTextRow !== null ) { |
||
| 1612 | $row = $this->mTextRow; |
||
| 1613 | $this->mTextRow = null; |
||
| 1614 | } else { |
||
| 1615 | $row = null; |
||
| 1616 | } |
||
| 1617 | |||
| 1618 | // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables |
||
| 1619 | // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases. |
||
| 1620 | $flags = $this->mQueryFlags; |
||
| 1621 | $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) |
||
| 1622 | ? self::READ_LATEST_IMMUTABLE |
||
| 1623 | : 0; |
||
| 1624 | |||
| 1625 | list( $index, $options, $fallbackIndex, $fallbackOptions ) = |
||
| 1626 | DBAccessObjectUtils::getDBOptions( $flags ); |
||
| 1627 | |||
| 1628 | View Code Duplication | if ( !$row ) { |
|
| 1629 | // Text data is immutable; check replica DBs first. |
||
| 1630 | $row = wfGetDB( $index )->selectRow( |
||
| 1631 | 'text', |
||
| 1632 | [ 'old_text', 'old_flags' ], |
||
| 1633 | [ 'old_id' => $textId ], |
||
| 1634 | __METHOD__, |
||
| 1635 | $options |
||
| 1636 | ); |
||
| 1637 | } |
||
| 1638 | |||
| 1639 | // Fallback to DB_MASTER in some cases if the row was not found |
||
| 1640 | View Code Duplication | if ( !$row && $fallbackIndex !== null ) { |
|
| 1641 | // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row |
||
| 1642 | // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided. |
||
| 1643 | $row = wfGetDB( $fallbackIndex )->selectRow( |
||
| 1644 | 'text', |
||
| 1645 | [ 'old_text', 'old_flags' ], |
||
| 1646 | [ 'old_id' => $textId ], |
||
| 1647 | __METHOD__, |
||
| 1648 | $fallbackOptions |
||
| 1649 | ); |
||
| 1650 | } |
||
| 1651 | |||
| 1652 | if ( !$row ) { |
||
| 1653 | wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." ); |
||
| 1654 | } |
||
| 1655 | |||
| 1656 | $text = self::getRevisionText( $row ); |
||
| 1657 | if ( $row && $text === false ) { |
||
| 1658 | wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." ); |
||
| 1659 | } |
||
| 1660 | |||
| 1661 | return is_string( $text ) ? $text : false; |
||
| 1662 | } |
||
| 1663 | |||
| 1664 | /** |
||
| 1665 | * Create a new null-revision for insertion into a page's |
||
| 1666 | * history. This will not re-save the text, but simply refer |
||
| 1667 | * to the text from the previous version. |
||
| 1668 | * |
||
| 1669 | * Such revisions can for instance identify page rename |
||
| 1670 | * operations and other such meta-modifications. |
||
| 1671 | * |
||
| 1672 | * @param IDatabase $dbw |
||
| 1673 | * @param int $pageId ID number of the page to read from |
||
| 1674 | * @param string $summary Revision's summary |
||
| 1675 | * @param bool $minor Whether the revision should be considered as minor |
||
| 1676 | * @param User|null $user User object to use or null for $wgUser |
||
| 1677 | * @return Revision|null Revision or null on error |
||
| 1678 | */ |
||
| 1679 | public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) { |
||
| 1680 | global $wgContentHandlerUseDB, $wgContLang; |
||
| 1681 | |||
| 1682 | $fields = [ 'page_latest', 'page_namespace', 'page_title', |
||
| 1683 | 'rev_text_id', 'rev_len', 'rev_sha1' ]; |
||
| 1684 | |||
| 1685 | if ( $wgContentHandlerUseDB ) { |
||
| 1686 | $fields[] = 'rev_content_model'; |
||
| 1687 | $fields[] = 'rev_content_format'; |
||
| 1688 | } |
||
| 1689 | |||
| 1690 | $current = $dbw->selectRow( |
||
| 1691 | [ 'page', 'revision' ], |
||
| 1692 | $fields, |
||
| 1693 | [ |
||
| 1694 | 'page_id' => $pageId, |
||
| 1695 | 'page_latest=rev_id', |
||
| 1696 | ], |
||
| 1697 | __METHOD__, |
||
| 1698 | [ 'FOR UPDATE' ] // T51581 |
||
| 1699 | ); |
||
| 1700 | |||
| 1701 | if ( $current ) { |
||
| 1702 | if ( !$user ) { |
||
| 1703 | global $wgUser; |
||
| 1704 | $user = $wgUser; |
||
| 1705 | } |
||
| 1706 | |||
| 1707 | // Truncate for whole multibyte characters |
||
| 1708 | $summary = $wgContLang->truncate( $summary, 255 ); |
||
| 1709 | |||
| 1710 | $row = [ |
||
| 1711 | 'page' => $pageId, |
||
| 1712 | 'user_text' => $user->getName(), |
||
| 1713 | 'user' => $user->getId(), |
||
| 1714 | 'comment' => $summary, |
||
| 1715 | 'minor_edit' => $minor, |
||
| 1716 | 'text_id' => $current->rev_text_id, |
||
| 1717 | 'parent_id' => $current->page_latest, |
||
| 1718 | 'len' => $current->rev_len, |
||
| 1719 | 'sha1' => $current->rev_sha1 |
||
| 1720 | ]; |
||
| 1721 | |||
| 1722 | if ( $wgContentHandlerUseDB ) { |
||
| 1723 | $row['content_model'] = $current->rev_content_model; |
||
| 1724 | $row['content_format'] = $current->rev_content_format; |
||
| 1725 | } |
||
| 1726 | |||
| 1727 | $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title ); |
||
| 1728 | |||
| 1729 | $revision = new Revision( $row ); |
||
| 1730 | } else { |
||
| 1731 | $revision = null; |
||
| 1732 | } |
||
| 1733 | |||
| 1734 | return $revision; |
||
| 1735 | } |
||
| 1736 | |||
| 1737 | /** |
||
| 1738 | * Determine if the current user is allowed to view a particular |
||
| 1739 | * field of this revision, if it's marked as deleted. |
||
| 1740 | * |
||
| 1741 | * @param int $field One of self::DELETED_TEXT, |
||
| 1742 | * self::DELETED_COMMENT, |
||
| 1743 | * self::DELETED_USER |
||
| 1744 | * @param User|null $user User object to check, or null to use $wgUser |
||
| 1745 | * @return bool |
||
| 1746 | */ |
||
| 1747 | public function userCan( $field, User $user = null ) { |
||
| 1748 | return self::userCanBitfield( $this->getVisibility(), $field, $user ); |
||
| 1749 | } |
||
| 1750 | |||
| 1751 | /** |
||
| 1752 | * Determine if the current user is allowed to view a particular |
||
| 1753 | * field of this revision, if it's marked as deleted. This is used |
||
| 1754 | * by various classes to avoid duplication. |
||
| 1755 | * |
||
| 1756 | * @param int $bitfield Current field |
||
| 1757 | * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE, |
||
| 1758 | * self::DELETED_COMMENT = File::DELETED_COMMENT, |
||
| 1759 | * self::DELETED_USER = File::DELETED_USER |
||
| 1760 | * @param User|null $user User object to check, or null to use $wgUser |
||
| 1761 | * @param Title|null $title A Title object to check for per-page restrictions on, |
||
| 1762 | * instead of just plain userrights |
||
| 1763 | * @return bool |
||
| 1764 | */ |
||
| 1765 | public static function userCanBitfield( $bitfield, $field, User $user = null, |
||
| 1766 | Title $title = null |
||
| 1767 | ) { |
||
| 1768 | if ( $bitfield & $field ) { // aspect is deleted |
||
| 1769 | if ( $user === null ) { |
||
| 1770 | global $wgUser; |
||
| 1771 | $user = $wgUser; |
||
| 1772 | } |
||
| 1773 | if ( $bitfield & self::DELETED_RESTRICTED ) { |
||
| 1774 | $permissions = [ 'suppressrevision', 'viewsuppressed' ]; |
||
| 1775 | } elseif ( $field & self::DELETED_TEXT ) { |
||
| 1776 | $permissions = [ 'deletedtext' ]; |
||
| 1777 | } else { |
||
| 1778 | $permissions = [ 'deletedhistory' ]; |
||
| 1779 | } |
||
| 1780 | $permissionlist = implode( ', ', $permissions ); |
||
| 1781 | if ( $title === null ) { |
||
| 1782 | wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); |
||
| 1783 | return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions ); |
||
| 1784 | } else { |
||
| 1785 | $text = $title->getPrefixedText(); |
||
| 1786 | wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" ); |
||
| 1787 | foreach ( $permissions as $perm ) { |
||
| 1788 | if ( $title->userCan( $perm, $user ) ) { |
||
| 1789 | return true; |
||
| 1790 | } |
||
| 1791 | } |
||
| 1792 | return false; |
||
| 1793 | } |
||
| 1794 | } else { |
||
| 1795 | return true; |
||
| 1796 | } |
||
| 1797 | } |
||
| 1798 | |||
| 1799 | /** |
||
| 1800 | * Get rev_timestamp from rev_id, without loading the rest of the row |
||
| 1801 | * |
||
| 1802 | * @param Title $title |
||
| 1803 | * @param int $id |
||
| 1804 | * @return string|bool False if not found |
||
| 1805 | */ |
||
| 1806 | static function getTimestampFromId( $title, $id, $flags = 0 ) { |
||
| 1807 | $db = ( $flags & self::READ_LATEST ) |
||
| 1808 | ? wfGetDB( DB_MASTER ) |
||
| 1809 | : wfGetDB( DB_REPLICA ); |
||
| 1810 | // Casting fix for databases that can't take '' for rev_id |
||
| 1811 | if ( $id == '' ) { |
||
| 1812 | $id = 0; |
||
| 1813 | } |
||
| 1814 | $conds = [ 'rev_id' => $id ]; |
||
| 1815 | $conds['rev_page'] = $title->getArticleID(); |
||
| 1816 | $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); |
||
| 1817 | |||
| 1818 | return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false; |
||
| 1819 | } |
||
| 1820 | |||
| 1821 | /** |
||
| 1822 | * Get count of revisions per page...not very efficient |
||
| 1823 | * |
||
| 1824 | * @param IDatabase $db |
||
| 1825 | * @param int $id Page id |
||
| 1826 | * @return int |
||
| 1827 | */ |
||
| 1828 | static function countByPageId( $db, $id ) { |
||
| 1829 | $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ], |
||
| 1830 | [ 'rev_page' => $id ], __METHOD__ ); |
||
| 1831 | if ( $row ) { |
||
| 1832 | return $row->revCount; |
||
| 1833 | } |
||
| 1834 | return 0; |
||
| 1835 | } |
||
| 1836 | |||
| 1837 | /** |
||
| 1838 | * Get count of revisions per page...not very efficient |
||
| 1839 | * |
||
| 1840 | * @param IDatabase $db |
||
| 1841 | * @param Title $title |
||
| 1842 | * @return int |
||
| 1843 | */ |
||
| 1844 | static function countByTitle( $db, $title ) { |
||
| 1845 | $id = $title->getArticleID(); |
||
| 1846 | if ( $id ) { |
||
| 1847 | return self::countByPageId( $db, $id ); |
||
| 1848 | } |
||
| 1849 | return 0; |
||
| 1850 | } |
||
| 1851 | |||
| 1852 | /** |
||
| 1853 | * Check if no edits were made by other users since |
||
| 1854 | * the time a user started editing the page. Limit to |
||
| 1855 | * 50 revisions for the sake of performance. |
||
| 1856 | * |
||
| 1857 | * @since 1.20 |
||
| 1858 | * @deprecated since 1.24 |
||
| 1859 | * |
||
| 1860 | * @param IDatabase|int $db The Database to perform the check on. May be given as a |
||
| 1861 | * Database object or a database identifier usable with wfGetDB. |
||
| 1862 | * @param int $pageId The ID of the page in question |
||
| 1863 | * @param int $userId The ID of the user in question |
||
| 1864 | * @param string $since Look at edits since this time |
||
| 1865 | * |
||
| 1866 | * @return bool True if the given user was the only one to edit since the given timestamp |
||
| 1867 | */ |
||
| 1868 | public static function userWasLastToEdit( $db, $pageId, $userId, $since ) { |
||
| 1869 | if ( !$userId ) { |
||
| 1870 | return false; |
||
| 1871 | } |
||
| 1872 | |||
| 1873 | if ( is_int( $db ) ) { |
||
| 1874 | $db = wfGetDB( $db ); |
||
| 1875 | } |
||
| 1876 | |||
| 1877 | $res = $db->select( 'revision', |
||
| 1878 | 'rev_user', |
||
| 1879 | [ |
||
| 1880 | 'rev_page' => $pageId, |
||
| 1881 | 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) ) |
||
| 1882 | ], |
||
| 1883 | __METHOD__, |
||
| 1884 | [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] ); |
||
| 1885 | foreach ( $res as $row ) { |
||
| 1886 | if ( $row->rev_user != $userId ) { |
||
| 1887 | return false; |
||
| 1888 | } |
||
| 1889 | } |
||
| 1890 | return true; |
||
| 1891 | } |
||
| 1892 | |||
| 1893 | /** |
||
| 1894 | * Load a revision based on a known page ID and current revision ID from the DB |
||
| 1895 | * |
||
| 1896 | * This method allows for the use of caching, though accessing anything that normally |
||
| 1897 | * requires permission checks (aside from the text) will trigger a small DB lookup. |
||
| 1898 | * The title will also be lazy loaded, though setTitle() can be used to preload it. |
||
| 1899 | * |
||
| 1900 | * @param IDatabase $db |
||
| 1901 | * @param int $pageId Page ID |
||
| 1902 | * @param int $revId Known current revision of this page |
||
| 1903 | * @return Revision|bool Returns false if missing |
||
| 1904 | * @since 1.28 |
||
| 1905 | */ |
||
| 1906 | public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) { |
||
| 1907 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
||
| 1908 | return $cache->getWithSetCallback( |
||
| 1909 | // Page/rev IDs passed in from DB to reflect history merges |
||
| 1910 | $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ), |
||
| 1911 | $cache::TTL_WEEK, |
||
| 1912 | function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) { |
||
| 1913 | $setOpts += Database::getCacheSetOptions( $db ); |
||
| 1914 | |||
| 1915 | $rev = Revision::loadFromPageId( $db, $pageId, $revId ); |
||
| 1916 | // Reflect revision deletion and user renames |
||
| 1917 | if ( $rev ) { |
||
| 1918 | $rev->mTitle = null; // mutable; lazy-load |
||
| 1919 | $rev->mRefreshMutableFields = true; |
||
| 1920 | } |
||
| 1921 | |||
| 1922 | return $rev ?: false; // don't cache negatives |
||
| 1923 | } |
||
| 1924 | ); |
||
| 1925 | } |
||
| 1926 | |||
| 1927 | /** |
||
| 1928 | * For cached revisions, make sure the user name and rev_deleted is up-to-date |
||
| 1929 | */ |
||
| 1930 | private function loadMutableFields() { |
||
| 1931 | if ( !$this->mRefreshMutableFields ) { |
||
| 1932 | return; // not needed |
||
| 1933 | } |
||
| 1934 | |||
| 1935 | $this->mRefreshMutableFields = false; |
||
| 1936 | $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki ); |
||
| 1937 | $row = $dbr->selectRow( |
||
| 1938 | [ 'revision', 'user' ], |
||
| 1939 | [ 'rev_deleted', 'user_name' ], |
||
| 1940 | [ 'rev_id' => $this->mId, 'user_id = rev_user' ], |
||
| 1941 | __METHOD__ |
||
| 1942 | ); |
||
| 1943 | if ( $row ) { // update values |
||
| 1944 | $this->mDeleted = (int)$row->rev_deleted; |
||
| 1945 | $this->mUserText = $row->user_name; |
||
| 1946 | } |
||
| 1947 | } |
||
| 1948 | } |
||
| 1949 |
In PHP, under loose comparison (like
==, or!=, orswitchconditions), values of different types might be equal.For
integervalues, zero is a special case, in particular the following results might be unexpected: