Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
75 / 75 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
ArchivedRevisionLookup | |
100.00% |
75 / 75 |
|
100.00% |
8 / 8 |
19 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
listRevisions | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getRevisionRecordByTimestamp | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getArchivedRevisionRecord | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRevisionByConditions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getPreviousRevisionRecord | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
8 | |||
getLastRevisionId | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
hasArchivedRevisions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Revision; |
22 | |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Page\PageIdentity; |
25 | use Wikimedia\Rdbms\IConnectionProvider; |
26 | use Wikimedia\Rdbms\IResultWrapper; |
27 | use Wikimedia\Rdbms\SelectQueryBuilder; |
28 | |
29 | /** |
30 | * @since 1.38 |
31 | */ |
32 | class ArchivedRevisionLookup { |
33 | |
34 | /** @var IConnectionProvider */ |
35 | private $dbProvider; |
36 | |
37 | /** @var RevisionStore */ |
38 | private $revisionStore; |
39 | |
40 | /** |
41 | * @param IConnectionProvider $dbProvider |
42 | * @param RevisionStore $revisionStore |
43 | */ |
44 | public function __construct( |
45 | IConnectionProvider $dbProvider, |
46 | RevisionStore $revisionStore |
47 | ) { |
48 | $this->dbProvider = $dbProvider; |
49 | $this->revisionStore = $revisionStore; |
50 | } |
51 | |
52 | /** |
53 | * List the revisions of the given page. Returns result wrapper with |
54 | * various archive table fields. |
55 | * |
56 | * @param PageIdentity $page |
57 | * @param array $extraConds Extra conditions to be added to the query |
58 | * @param ?int $limit The limit to be applied to the query, or null for no limit |
59 | * @return IResultWrapper |
60 | */ |
61 | public function listRevisions( PageIdentity $page, array $extraConds = [], ?int $limit = null ) { |
62 | $queryBuilder = $this->revisionStore->newArchiveSelectQueryBuilder( $this->dbProvider->getReplicaDatabase() ) |
63 | ->joinComment() |
64 | ->where( $extraConds ) |
65 | ->andWhere( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ); |
66 | |
67 | // NOTE: ordering by ar_timestamp and ar_id, to remove ambiguity. |
68 | // XXX: Ideally, we would be ordering by ar_timestamp and ar_rev_id, but since we |
69 | // don't have an index on ar_rev_id, that causes a file sort. |
70 | $queryBuilder->orderBy( [ 'ar_timestamp', 'ar_id' ], SelectQueryBuilder::SORT_DESC ); |
71 | if ( $limit !== null ) { |
72 | $queryBuilder->limit( $limit ); |
73 | } |
74 | |
75 | MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQueryBuilder( $queryBuilder, 'archive' ); |
76 | |
77 | return $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
78 | } |
79 | |
80 | /** |
81 | * Return a RevisionRecord object containing data for the deleted revision. |
82 | * |
83 | * @internal only for use in SpecialUndelete |
84 | * |
85 | * @param PageIdentity $page |
86 | * @param string $timestamp |
87 | * @return RevisionRecord|null |
88 | */ |
89 | public function getRevisionRecordByTimestamp( PageIdentity $page, $timestamp ): ?RevisionRecord { |
90 | return $this->getRevisionByConditions( |
91 | $page, |
92 | [ 'ar_timestamp' => $this->dbProvider->getReplicaDatabase()->timestamp( $timestamp ) ] |
93 | ); |
94 | } |
95 | |
96 | /** |
97 | * Return the archived revision with the given ID. |
98 | * |
99 | * @param PageIdentity|null $page |
100 | * @param int $revId |
101 | * @return RevisionRecord|null |
102 | */ |
103 | public function getArchivedRevisionRecord( ?PageIdentity $page, int $revId ): ?RevisionRecord { |
104 | return $this->getRevisionByConditions( $page, [ 'ar_rev_id' => $revId ] ); |
105 | } |
106 | |
107 | /** |
108 | * @param PageIdentity|null $page |
109 | * @param array $conditions |
110 | * @param array $options |
111 | * |
112 | * @return RevisionRecord|null |
113 | */ |
114 | private function getRevisionByConditions( |
115 | ?PageIdentity $page, |
116 | array $conditions, |
117 | array $options = [] |
118 | ): ?RevisionRecord { |
119 | $queryBuilder = $this->revisionStore->newArchiveSelectQueryBuilder( $this->dbProvider->getReplicaDatabase() ) |
120 | ->joinComment() |
121 | ->where( $conditions ) |
122 | ->options( $options ); |
123 | |
124 | if ( $page ) { |
125 | $queryBuilder->andWhere( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ); |
126 | } |
127 | |
128 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
129 | |
130 | if ( $row ) { |
131 | return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $page ); |
132 | } |
133 | |
134 | return null; |
135 | } |
136 | |
137 | /** |
138 | * Return the most-previous revision, either live or deleted, against |
139 | * the deleted revision given by timestamp. |
140 | * |
141 | * May produce unexpected results in case of history merges or other |
142 | * unusual time issues. |
143 | * |
144 | * @param PageIdentity $page |
145 | * @param string $timestamp |
146 | * @return RevisionRecord|null Null when there is no previous revision |
147 | */ |
148 | public function getPreviousRevisionRecord( PageIdentity $page, string $timestamp ): ?RevisionRecord { |
149 | $dbr = $this->dbProvider->getReplicaDatabase(); |
150 | |
151 | // Check the previous deleted revision... |
152 | $row = $dbr->newSelectQueryBuilder() |
153 | ->select( [ 'ar_rev_id', 'ar_timestamp' ] ) |
154 | ->from( 'archive' ) |
155 | ->where( [ |
156 | 'ar_namespace' => $page->getNamespace(), |
157 | 'ar_title' => $page->getDBkey(), |
158 | $dbr->expr( 'ar_timestamp', '<', $dbr->timestamp( $timestamp ) ), |
159 | ] ) |
160 | ->orderBy( 'ar_timestamp DESC' ) |
161 | ->caller( __METHOD__ )->fetchRow(); |
162 | $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false; |
163 | $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null; |
164 | |
165 | $row = $dbr->newSelectQueryBuilder() |
166 | ->select( [ 'rev_id', 'rev_timestamp' ] ) |
167 | ->from( 'page' ) |
168 | ->join( 'revision', null, 'page_id = rev_page' ) |
169 | ->where( [ |
170 | 'page_namespace' => $page->getNamespace(), |
171 | 'page_title' => $page->getDBkey(), |
172 | $dbr->expr( 'rev_timestamp', '<', $dbr->timestamp( $timestamp ) ) |
173 | ] ) |
174 | ->orderBy( 'rev_timestamp DESC' ) |
175 | ->caller( __METHOD__ )->fetchRow(); |
176 | $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false; |
177 | $prevLiveId = $row ? intval( $row->rev_id ) : null; |
178 | |
179 | if ( $prevLive && $prevLive > $prevDeleted ) { |
180 | // Most prior revision was live |
181 | $rec = $this->revisionStore->getRevisionById( $prevLiveId ); |
182 | } elseif ( $prevDeleted ) { |
183 | // Most prior revision was deleted |
184 | $rec = $this->getArchivedRevisionRecord( $page, $prevDeletedId ); |
185 | } else { |
186 | $rec = null; |
187 | } |
188 | |
189 | return $rec; |
190 | } |
191 | |
192 | /** |
193 | * Returns the ID of the latest deleted revision. |
194 | * |
195 | * @param PageIdentity $page |
196 | * |
197 | * @return int|false The revision's ID, or false if there is no deleted revision. |
198 | */ |
199 | public function getLastRevisionId( PageIdentity $page ) { |
200 | $revId = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
201 | ->select( 'ar_rev_id' ) |
202 | ->from( 'archive' ) |
203 | ->where( [ 'ar_namespace' => $page->getNamespace(), 'ar_title' => $page->getDBkey() ] ) |
204 | ->orderBy( [ 'ar_timestamp', 'ar_id' ], SelectQueryBuilder::SORT_DESC ) |
205 | ->caller( __METHOD__ )->fetchField(); |
206 | |
207 | return $revId ? intval( $revId ) : false; |
208 | } |
209 | |
210 | /** |
211 | * Quick check if any archived revisions are present for the page. |
212 | * This says nothing about whether the page currently exists in the page table or not. |
213 | * |
214 | * @param PageIdentity $page |
215 | * |
216 | * @return bool |
217 | */ |
218 | public function hasArchivedRevisions( PageIdentity $page ): bool { |
219 | $row = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
220 | ->select( '1' ) // We don't care about the value. Allow the database to optimize. |
221 | ->from( 'archive' ) |
222 | ->where( [ |
223 | 'ar_namespace' => $page->getNamespace(), |
224 | 'ar_title' => $page->getDBkey() |
225 | ] ) |
226 | ->caller( __METHOD__ ) |
227 | ->fetchRow(); |
228 | |
229 | return (bool)$row; |
230 | } |
231 | |
232 | } |