Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 147 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ContributionsQuery | |
0.00% |
0 / 147 |
|
0.00% |
0 / 7 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getResults | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
182 | |||
excludeFromContributions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildConditions | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
132 | |||
queryRevisions | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
30 | |||
loadRevisions | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
validate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace Flow\Formatter; |
4 | |
5 | use Flow\Data\ManagerGroup; |
6 | use Flow\Data\Storage\RevisionStorage; |
7 | use Flow\DbFactory; |
8 | use Flow\Exception\FlowException; |
9 | use Flow\FlowActions; |
10 | use Flow\Model\AbstractRevision; |
11 | use Flow\Model\UUID; |
12 | use Flow\Repository\TreeRepository; |
13 | use InvalidArgumentException; |
14 | use MediaWiki\Pager\ContribsPager; |
15 | use MediaWiki\Pager\DeletedContribsPager; |
16 | use MediaWiki\User\UserIdentityLookup; |
17 | use MediaWiki\WikiMap\WikiMap; |
18 | use Wikimedia\Rdbms\IResultWrapper; |
19 | |
20 | class ContributionsQuery extends AbstractQuery { |
21 | |
22 | /** |
23 | * @var DbFactory |
24 | */ |
25 | protected $dbFactory; |
26 | |
27 | /** |
28 | * @var FlowActions |
29 | */ |
30 | protected $actions; |
31 | |
32 | /** @var UserIdentityLookup */ |
33 | private $userIdentityLookup; |
34 | |
35 | /** |
36 | * @param ManagerGroup $storage |
37 | * @param TreeRepository $treeRepo |
38 | * @param DbFactory $dbFactory |
39 | * @param FlowActions $actions |
40 | * @param UserIdentityLookup $userIdentityLookup |
41 | */ |
42 | public function __construct( |
43 | ManagerGroup $storage, |
44 | TreeRepository $treeRepo, |
45 | DbFactory $dbFactory, |
46 | FlowActions $actions, |
47 | UserIdentityLookup $userIdentityLookup |
48 | ) { |
49 | parent::__construct( $storage, $treeRepo ); |
50 | $this->dbFactory = $dbFactory; |
51 | $this->actions = $actions; |
52 | $this->userIdentityLookup = $userIdentityLookup; |
53 | } |
54 | |
55 | /** |
56 | * @param ContribsPager|DeletedContribsPager $pager Object hooked into |
57 | * @param string $offset Index offset, inclusive |
58 | * @param int $limit Exact query limit |
59 | * @param bool $descending Query direction, false for ascending, true for descending |
60 | * @param array $rangeOffsets Query range, in the format of [ endOffset, startOffset ] |
61 | * @return FormatterRow[] |
62 | */ |
63 | public function getResults( $pager, $offset, $limit, $descending, $rangeOffsets = [] ) { |
64 | // When ORES hidenondamaging filter is used, Flow entries should be skipped |
65 | // because they are not scored. |
66 | if ( $pager->getRequest()->getBool( 'hidenondamaging' ) ) { |
67 | return []; |
68 | } |
69 | |
70 | // build DB query conditions |
71 | $conditions = $this->buildConditions( $pager, $offset, $descending, $rangeOffsets ); |
72 | |
73 | $types = [ |
74 | // revision class => block type |
75 | 'PostRevision' => 'topic', |
76 | 'Header' => 'header', |
77 | 'PostSummary' => 'topicsummary' |
78 | ]; |
79 | |
80 | $results = []; |
81 | foreach ( $types as $revisionClass => $blockType ) { |
82 | // query DB for requested revisions |
83 | $rows = $this->queryRevisions( $conditions, $limit, $revisionClass ); |
84 | |
85 | // turn DB data into revision objects |
86 | $revisions = $this->loadRevisions( $rows, $revisionClass ); |
87 | |
88 | $this->loadMetadataBatch( $revisions ); |
89 | foreach ( $revisions as $revision ) { |
90 | try { |
91 | if ( $this->excludeFromContributions( $revision ) ) { |
92 | continue; |
93 | } |
94 | |
95 | $result = $pager instanceof ContribsPager ? new ContributionsRow : new DeletedContributionsRow; |
96 | $result = $this->buildResult( $revision, $pager->getIndexField(), $result ); |
97 | $deleted = $result->currentRevision->isDeleted() || $result->workflow->isDeleted(); |
98 | |
99 | if ( |
100 | $result instanceof ContributionsRow && |
101 | ( $deleted || $result->currentRevision->isSuppressed() ) |
102 | ) { |
103 | // don't show deleted or suppressed entries in Special:Contributions |
104 | continue; |
105 | } |
106 | if ( $result instanceof DeletedContributionsRow && !$deleted ) { |
107 | // only show deleted entries in Special:DeletedContributions |
108 | continue; |
109 | } |
110 | |
111 | $results[] = $result; |
112 | } catch ( FlowException $e ) { |
113 | \MWExceptionHandler::logException( $e ); |
114 | } |
115 | } |
116 | } |
117 | |
118 | return $results; |
119 | } |
120 | |
121 | /** |
122 | * @param AbstractRevision $revision |
123 | * @return bool |
124 | */ |
125 | private function excludeFromContributions( AbstractRevision $revision ) { |
126 | return (bool)$this->actions->getValue( $revision->getChangeType(), 'exclude_from_contributions' ); |
127 | } |
128 | |
129 | /** |
130 | * @param ContribsPager|DeletedContribsPager $pager Object hooked into |
131 | * @param string $offset Index offset, inclusive |
132 | * @param bool $descending Query direction, false for ascending, true for descending |
133 | * @param array $rangeOffsets Query range, in the format of [ endOffset, startOffset ] |
134 | * @return array Query conditions |
135 | */ |
136 | protected function buildConditions( $pager, $offset, $descending, $rangeOffsets = [] ) { |
137 | $conditions = []; |
138 | |
139 | $isContribsPager = $pager instanceof ContribsPager; |
140 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $pager->getTarget() ); |
141 | if ( $userIdentity && $userIdentity->isRegistered() ) { |
142 | $conditions['rev_user_id'] = $userIdentity->getId(); |
143 | $conditions['rev_user_ip'] = null; |
144 | $conditions['rev_user_wiki'] = WikiMap::getCurrentWikiId(); |
145 | } else { |
146 | $conditions['rev_user_id'] = 0; |
147 | $conditions['rev_user_ip'] = $pager->getTarget(); |
148 | $conditions['rev_user_wiki'] = WikiMap::getCurrentWikiId(); |
149 | } |
150 | |
151 | if ( $isContribsPager && $pager->isNewOnly() ) { |
152 | $conditions['rev_parent_id'] = null; |
153 | $conditions['rev_type'] = 'post'; |
154 | } |
155 | |
156 | $dbr = $this->dbFactory->getDB( DB_REPLICA ); |
157 | // Make offset parameter. |
158 | if ( $offset ) { |
159 | $offsetUUID = UUID::getComparisonUUID( $offset ); |
160 | $direction = $descending ? '>' : '<'; |
161 | $conditions[] = $dbr->buildComparison( $direction, [ 'rev_id' => $offsetUUID->getBinary() ] ); |
162 | } |
163 | if ( $rangeOffsets ) { |
164 | $endUUID = UUID::getComparisonUUID( $rangeOffsets[0] ); |
165 | $conditions[] = $dbr->buildComparison( '<', [ 'rev_id' => $endUUID->getBinary() ] ); |
166 | // The DeletedContribsPager is only a ReverseChronologicalPager for now. |
167 | if ( count( $rangeOffsets ) > 1 && $rangeOffsets[1] ) { |
168 | $startUUID = UUID::getComparisonUUID( $rangeOffsets[1] ); |
169 | $conditions[] = $dbr->buildComparison( '>=', [ 'rev_id' => $startUUID->getBinary() ] ); |
170 | } |
171 | } |
172 | |
173 | // Find only within requested wiki/namespace |
174 | $conditions['workflow_wiki'] = WikiMap::getCurrentWikiId(); |
175 | if ( $pager->getNamespace() !== '' ) { |
176 | $conditions['workflow_namespace'] = $pager->getNamespace(); |
177 | } |
178 | |
179 | return $conditions; |
180 | } |
181 | |
182 | /** |
183 | * @param array $conditions |
184 | * @param int $limit |
185 | * @param string $revisionClass Storage type (e.g. "PostRevision", "Header") |
186 | * @return IResultWrapper |
187 | */ |
188 | protected function queryRevisions( $conditions, $limit, $revisionClass ) { |
189 | $dbr = $this->dbFactory->getDB( DB_REPLICA ); |
190 | |
191 | switch ( $revisionClass ) { |
192 | case 'PostRevision': |
193 | return $dbr->select( |
194 | [ |
195 | 'flow_revision', // revisions to find |
196 | 'flow_tree_revision', // resolve to post id |
197 | 'flow_tree_node', // resolve to root post (topic title) |
198 | 'flow_workflow', // resolve to workflow, to test if in correct wiki/namespace |
199 | ], |
200 | [ '*' ], |
201 | $conditions, |
202 | __METHOD__, |
203 | [ |
204 | 'LIMIT' => $limit, |
205 | 'ORDER BY' => 'rev_id DESC', |
206 | ], |
207 | [ |
208 | 'flow_tree_revision' => [ |
209 | 'INNER JOIN', |
210 | [ 'tree_rev_id = rev_id' ] |
211 | ], |
212 | 'flow_tree_node' => [ |
213 | 'INNER JOIN', |
214 | [ |
215 | 'tree_descendant_id = tree_rev_descendant_id', |
216 | // the one with max tree_depth will be root, |
217 | // which will have the matching workflow id |
218 | ] |
219 | ], |
220 | 'flow_workflow' => [ |
221 | 'INNER JOIN', |
222 | [ 'workflow_id = tree_ancestor_id' ] |
223 | ], |
224 | ] |
225 | ); |
226 | |
227 | case 'Header': |
228 | return $dbr->select( |
229 | [ 'flow_revision', 'flow_workflow' ], |
230 | [ '*' ], |
231 | $conditions, |
232 | __METHOD__, |
233 | [ |
234 | 'LIMIT' => $limit, |
235 | 'ORDER BY' => 'rev_id DESC', |
236 | ], |
237 | [ |
238 | 'flow_workflow' => [ |
239 | 'INNER JOIN', |
240 | [ 'workflow_id = rev_type_id', 'rev_type' => 'header' ] |
241 | ], |
242 | ] |
243 | ); |
244 | |
245 | case 'PostSummary': |
246 | return $dbr->select( |
247 | [ 'flow_revision', 'flow_tree_node', 'flow_workflow' ], |
248 | [ '*' ], |
249 | $conditions, |
250 | __METHOD__, |
251 | [ |
252 | 'LIMIT' => $limit, |
253 | 'ORDER BY' => 'rev_id DESC', |
254 | ], |
255 | [ |
256 | 'flow_tree_node' => [ |
257 | 'INNER JOIN', |
258 | [ 'tree_descendant_id = rev_type_id', 'rev_type' => 'post-summary' ] |
259 | ], |
260 | 'flow_workflow' => [ |
261 | 'INNER JOIN', |
262 | [ 'workflow_id = tree_ancestor_id' ] |
263 | ] |
264 | ] |
265 | ); |
266 | |
267 | default: |
268 | throw new InvalidArgumentException( 'Unsupported revision type ' . $revisionClass ); |
269 | } |
270 | } |
271 | |
272 | /** |
273 | * Turns DB data into revision objects. |
274 | * |
275 | * @param IResultWrapper $rows |
276 | * @param string $revisionClass Class of revision object to build: PostRevision|Header |
277 | * @return array |
278 | */ |
279 | protected function loadRevisions( IResultWrapper $rows, $revisionClass ) { |
280 | $revisions = []; |
281 | foreach ( $rows as $row ) { |
282 | $revisions[UUID::create( $row->rev_id )->getAlphadecimal()] = (array)$row; |
283 | } |
284 | |
285 | // get content in external storage |
286 | $res = [ $revisions ]; |
287 | $res = RevisionStorage::mergeExternalContent( $res ); |
288 | $revisions = reset( $res ); |
289 | |
290 | // we have all required data to build revision |
291 | $mapper = $this->storage->getStorage( $revisionClass )->getMapper(); |
292 | $revisions = array_map( [ $mapper, 'fromStorageRow' ], $revisions ); |
293 | |
294 | // @todo: we may already be able to build workflowCache (and rootPostIdCache) from this DB data |
295 | |
296 | return $revisions; |
297 | } |
298 | |
299 | /** |
300 | * When retrieving revisions from DB, self::mergeExternalContent will be |
301 | * called to fetch the content. This could fail, resulting in the content |
302 | * being a 'false' value. |
303 | * |
304 | * @inheritDoc |
305 | */ |
306 | public function validate( array $row ) { |
307 | return !isset( $row['rev_content'] ) || $row['rev_content'] !== false; |
308 | } |
309 | } |