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 ) { |
||
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(); |
||
0 ignored issues
–
show
|
|||
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 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.