Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TopicListQuery
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 8
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getResults
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
132
 getTopicIds
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 collectPostIds
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 collectWatchStatus
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 collectSummary
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 collectRevisions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getCurrentRevision
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Formatter;
4
5use Flow\Data\ManagerGroup;
6use Flow\Exception\FlowException;
7use Flow\Exception\InvalidDataException;
8use Flow\Model\AbstractRevision;
9use Flow\Model\PostRevision;
10use Flow\Model\PostSummary;
11use Flow\Model\TopicListEntry;
12use Flow\Model\UUID;
13use Flow\Repository\TreeRepository;
14use Flow\RevisionActionPermissions;
15use Flow\WatchedTopicItems;
16use MediaWiki\Exception\MWExceptionHandler;
17use MediaWiki\Json\FormatJson;
18
19class TopicListQuery extends AbstractQuery {
20
21    /** @var RevisionActionPermissions */
22    protected $permissions;
23    /** @var WatchedTopicItems */
24    protected $watchedTopicItems;
25
26    /**
27     * @param ManagerGroup $storage
28     * @param TreeRepository $treeRepository
29     * @param RevisionActionPermissions $permissions
30     * @param WatchedTopicItems $watchedTopicItems
31     */
32    public function __construct(
33        ManagerGroup $storage,
34        TreeRepository $treeRepository,
35        RevisionActionPermissions $permissions,
36        WatchedTopicItems $watchedTopicItems
37    ) {
38        parent::__construct( $storage, $treeRepository );
39        $this->permissions = $permissions;
40        $this->watchedTopicItems = $watchedTopicItems;
41    }
42
43    /**
44     * @param UUID[]|TopicListEntry[] $topicIdsOrEntries
45     * @return FormatterRow[]
46     * @suppress PhanUndeclaredMethod Types not inferred from instanceof
47     */
48    public function getResults( array $topicIdsOrEntries ) {
49        $topicIds = $this->getTopicIds( $topicIdsOrEntries );
50        $allPostIds = $this->collectPostIds( $topicIds );
51        $topicSummary = $this->collectSummary( $topicIds );
52        $posts = $this->collectRevisions( $allPostIds );
53        $watchStatus = $this->collectWatchStatus( $topicIds );
54
55        $missing = array_diff(
56            array_keys( $allPostIds ),
57            array_keys( $posts )
58        );
59        if ( $missing ) {
60            throw new InvalidDataException(
61                'Failed to load latest revision for post IDs: ' . FormatJson::encode( $missing ),
62                'fail-load-data'
63            );
64        }
65
66        $this->loadMetadataBatch( $posts );
67        $results = [];
68        $replies = [];
69        foreach ( $posts as $post ) {
70            try {
71                if ( !$this->permissions->isAllowed( $post, 'view' ) ) {
72                    continue;
73                }
74                $row = new TopicRow;
75                $this->buildResult( $post, null, $row );
76                /** @var PostRevision $revision */
77                $revision = $row->revision;
78                $replyToId = $revision->getReplyToId();
79                $replyToId = $replyToId ? $replyToId->getAlphadecimal() : null;
80                $postId = $revision->getPostId()->getAlphadecimal();
81                $replies[$replyToId] = $postId;
82                if ( $post->isTopicTitle() ) {
83                    // Attach the summary
84                    if ( isset( $topicSummary[$postId] ) ) {
85                        $row->summary = $this->buildResult( $topicSummary[$postId], 'rev_id' );
86                    }
87                    // Attach the watch status
88                    if ( isset( $watchStatus[$postId] ) && $watchStatus[$postId] ) {
89                        $row->isWatched = true;
90                    }
91                }
92                $results[] = $row;
93            } catch ( FlowException $e ) {
94                MWExceptionHandler::logException( $e );
95            }
96        }
97
98        foreach ( $results as $result ) {
99            $alpha = $result->revision->getPostId()->getAlphadecimal();
100            $result->replies = $replies[$alpha] ?? [];
101        }
102
103        return $results;
104    }
105
106    /**
107     * @param TopicListEntry[]|UUID[] $topicsIdsOrEntries Topic IDs as UUID entries or
108     *  TopicListEntry objects
109     * @return UUID[]
110     */
111    protected function getTopicIds( array $topicsIdsOrEntries ) {
112        $topicIds = [];
113        foreach ( $topicsIdsOrEntries as $entry ) {
114            if ( $entry instanceof UUID ) {
115                $topicIds[] = $entry;
116            } elseif ( $entry instanceof TopicListEntry ) {
117                $topicIds[] = $entry->getId();
118            }
119        }
120        return $topicIds;
121    }
122
123    /**
124     * @param UUID[] $topicIds
125     * @return UUID[] Indexed by alphadecimal representation
126     */
127    protected function collectPostIds( array $topicIds ) {
128        if ( !$topicIds ) {
129            return [];
130        }
131        // Get the full list of postId's necessary
132        $nodeList = $this->treeRepository->fetchSubtreeNodeList( $topicIds );
133
134        // Merge all the children from the various posts into one array
135        if ( !$nodeList ) {
136            // It should have returned at least $topicIds
137            wfDebugLog( 'Flow', __METHOD__ .
138                ': No result received from TreeRepository::fetchSubtreeNodeList' );
139            $postIds = $topicIds;
140        } elseif ( count( $nodeList ) === 1 ) {
141            $postIds = reset( $nodeList );
142        } else {
143            $postIds = array_merge( ...array_values( $nodeList ) );
144        }
145
146        // re-index by alphadecimal id
147        return array_combine(
148            array_map(
149                static function ( UUID $x ) {
150                    return $x->getAlphadecimal();
151                },
152                $postIds
153            ),
154            $postIds
155        );
156    }
157
158    /**
159     * @param UUID[] $topicIds
160     * @return array
161     */
162    protected function collectWatchStatus( $topicIds ) {
163        $ids = [];
164        foreach ( $topicIds as $topicId ) {
165            $ids[] = $topicId->getAlphadecimal();
166        }
167        return $this->watchedTopicItems->getWatchStatus( $ids );
168    }
169
170    /**
171     * @param UUID[] $topicIds
172     * @return PostSummary[]
173     */
174    protected function collectSummary( $topicIds ) {
175        if ( !$topicIds ) {
176            return [];
177        }
178        $conds = [];
179        foreach ( $topicIds as $topicId ) {
180            $conds[] = [ 'rev_type_id' => $topicId ];
181        }
182        $found = $this->storage->findMulti( 'PostSummary', $conds, [
183            'sort' => 'rev_id',
184            'order' => 'DESC',
185            'limit' => 1,
186        ] );
187        $result = [];
188        foreach ( $found as $row ) {
189            $summary = reset( $row );
190            $result[$summary->getSummaryTargetId()->getAlphadecimal()] = $summary;
191        }
192        return $result;
193    }
194
195    /**
196     * @param UUID[] $postIds
197     * @return PostRevision[] Indexed by alphadecimal post id
198     */
199    protected function collectRevisions( array $postIds ) {
200        $queries = [];
201        foreach ( $postIds as $postId ) {
202            $queries[] = [ 'rev_type_id' => $postId ];
203        }
204        $found = $this->storage->findMulti( 'PostRevision', $queries, [
205            'sort' => 'rev_id',
206            'order' => 'DESC',
207            'limit' => 1,
208        ] );
209
210        // index results by post id for later filtering
211        $result = [];
212        foreach ( $found as $row ) {
213            $revision = reset( $row );
214            $result[$revision->getPostId()->getAlphadecimal()] = $revision;
215        }
216
217        return $result;
218    }
219
220    /**
221     * Override parent, we only load the most recent version, so just
222     * return self.
223     * @param AbstractRevision $revision
224     * @return AbstractRevision
225     */
226    protected function getCurrentRevision( AbstractRevision $revision ) {
227        return $revision;
228    }
229}