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