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 ) { |
||
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 ) { |
|
0 ignored issues
–
show
|
|||
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!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: