Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContributionsQuery
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 7
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getResults
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
182
 excludeFromContributions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildConditions
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
132
 queryRevisions
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
30
 loadRevisions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 validate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace Flow\Formatter;
4
5use Flow\Data\ManagerGroup;
6use Flow\Data\Storage\RevisionStorage;
7use Flow\DbFactory;
8use Flow\Exception\FlowException;
9use Flow\FlowActions;
10use Flow\Model\AbstractRevision;
11use Flow\Model\UUID;
12use Flow\Repository\TreeRepository;
13use InvalidArgumentException;
14use MediaWiki\Pager\ContribsPager;
15use MediaWiki\Pager\DeletedContribsPager;
16use MediaWiki\User\UserIdentityLookup;
17use MediaWiki\WikiMap\WikiMap;
18use Wikimedia\Rdbms\IResultWrapper;
19use Wikimedia\Rdbms\SelectQueryBuilder;
20
21class 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}