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