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 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RootPostLoader
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 6
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getWithRoot
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 get
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMulti
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
380
 fetchRelatedPostIds
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getTreeRepo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Repository;
4
5use Flow\Data\ManagerGroup;
6use Flow\Exception\InvalidDataException;
7use Flow\Model\PostRevision;
8use Flow\Model\UUID;
9use FormatJson;
10use MediaWiki\User\User;
11
12/**
13 * I'm pretty sure this will generally work for any subtree, not just the topic
14 * root.  The problem is once you allow any subtree you need to handle the
15 * depth and root post setters better, they make the assumption the root provided
16 * is actually a root.
17 */
18class RootPostLoader {
19    /**
20     * @var ManagerGroup
21     */
22    protected $storage;
23
24    /**
25     * @var TreeRepository
26     */
27    protected $treeRepo;
28
29    /**
30     * @param ManagerGroup $storage
31     * @param TreeRepository $treeRepo
32     */
33    public function __construct( ManagerGroup $storage, TreeRepository $treeRepo ) {
34        $this->storage = $storage;
35        $this->treeRepo = $treeRepo;
36    }
37
38    /**
39     * Retrieves a single post and the related topic title.
40     *
41     * @param UUID|string $postId The uid of the post being requested
42     * @return (PostRevision|null)[] associative array with 'root' and 'post' keys. Array
43     *   values may be null if not found.
44     * @throws InvalidDataException
45     * @phan-return array{root:null|PostRevision,post:null|PostRevision}
46     */
47    public function getWithRoot( $postId ) {
48        $postId = UUID::create( $postId );
49        $rootId = $this->treeRepo->findRoot( $postId );
50        $found = $this->storage->findMulti(
51            'PostRevision',
52            [
53                [ 'rev_type_id' => $postId ],
54                [ 'rev_type_id' => $rootId ],
55            ],
56            [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ]
57        );
58        $res = [
59            'post' => null,
60            'root' => null,
61        ];
62        if ( !$found ) {
63            return $res;
64        }
65        foreach ( $found as $result ) {
66            // limit = 1 means single result
67            $post = reset( $result );
68            if ( $postId->equals( $post->getPostId() ) ) {
69                $res['post'] = $post;
70            } elseif ( $rootId->equals( $post->getPostId() ) ) {
71                $res['root'] = $post;
72            } else {
73                throw new InvalidDataException( 'Unmatched: ' . $post->getPostId()->getAlphadecimal() );
74            }
75        }
76        // The above doesn't catch this condition
77        if ( $postId->equals( $rootId ) ) {
78            $res['root'] = $res['post'];
79        }
80        return $res;
81    }
82
83    /**
84     * @param UUID $topicId
85     * @return PostRevision
86     * @throws InvalidDataException
87     */
88    public function get( $topicId ) {
89        $result = $this->getMulti( [ $topicId ] );
90        return reset( $result );
91    }
92
93    /**
94     * @param UUID[] $topicIds
95     * @return PostRevision[]
96     * @throws InvalidDataException
97     */
98    public function getMulti( array $topicIds ) {
99        if ( !$topicIds ) {
100            return [];
101        }
102        // load posts for all located post ids
103        $allPostIds = $this->fetchRelatedPostIds( $topicIds );
104        $queries = [];
105        foreach ( $allPostIds as $postId ) {
106            $queries[] = [ 'rev_type_id' => $postId ];
107        }
108        $found = $this->storage->findMulti( 'PostRevision', $queries, [
109            'sort' => 'rev_id',
110            'order' => 'DESC',
111            'limit' => 1,
112        ] );
113        /** @var PostRevision[] $posts */
114        $posts = $children = [];
115        foreach ( $found as $indexResult ) {
116            $post = reset( $indexResult ); // limit => 1 means only 1 result per query
117            if ( isset( $posts[$post->getPostId()->getAlphadecimal()] ) ) {
118                throw new InvalidDataException(
119                    'Multiple results for id: ' . $post->getPostId()->getAlphadecimal(),
120                    'fail-load-data'
121                );
122            }
123            $posts[$post->getPostId()->getAlphadecimal()] = $post;
124        }
125        $prettyPostIds = [];
126        foreach ( $allPostIds as $id ) {
127            $prettyPostIds[] = $id->getAlphadecimal();
128        }
129        $missing = array_diff( $prettyPostIds, array_keys( $posts ) );
130        if ( $missing ) {
131            // convert string uuid's into UUID objects
132            /** @var UUID[] $missingUUID */
133            $missingUUID = array_map( [ UUID::class, 'create' ], $missing );
134
135            // we'll need to know parents to add stub post correctly in post hierarchy
136            $parents = $this->treeRepo->fetchParentMap( $missingUUID );
137            $missingParents = array_diff( $missing, array_keys( $parents ) );
138            if ( $missingParents ) {
139                // if we can't fetch a post's original position in the tree
140                // hierarchy, we can't create a stub post to display, so bail
141                throw new InvalidDataException(
142                    'Missing Posts & parents: ' . json_encode( $missingParents ),
143                    'fail-load-data'
144                );
145            }
146
147            foreach ( $missingUUID as $postId ) {
148                $content = wfMessage( 'flow-stub-post-content' )->text();
149                $username = wfMessage( 'flow-system-usertext' )->text();
150                $user = User::newFromName( $username );
151
152                // create a stub post instead of failing completely
153                $post = PostRevision::newFromId( $postId, $user, $content, 'wikitext' );
154                $post->setReplyToId( $parents[$postId->getAlphadecimal()] );
155                $posts[$postId->getAlphadecimal()] = $post;
156
157                wfDebugLog( 'Flow', __METHOD__ . ': Missing posts: ' . FormatJson::encode( $missing ) );
158            }
159        }
160        // another helper to catch bugs in dev
161        $extra = array_diff( array_keys( $posts ), $prettyPostIds );
162        if ( $extra ) {
163            throw new InvalidDataException(
164                'Found unrequested posts: ' . FormatJson::encode( $extra ),
165                'fail-load-data'
166            );
167        }
168
169        // populate array of children
170        foreach ( $posts as $post ) {
171            if ( $post->getReplyToId() ) {
172                $children[$post->getReplyToId()->getAlphadecimal()][$post->getPostId()->getAlphadecimal()] = $post;
173            }
174        }
175        $extraParents = array_diff( array_keys( $children ), $prettyPostIds );
176        if ( $extraParents ) {
177            throw new InvalidDataException(
178                'Found posts with unrequested parents: ' . FormatJson::encode( $extraParents ),
179                'fail-load-data'
180            );
181        }
182
183        foreach ( $posts as $postId => $post ) {
184            $postChildren = [];
185            $postDepth = 0;
186
187            // link parents to their children
188            if ( isset( $children[$postId] ) ) {
189                // sort children with oldest items first
190                ksort( $children[$postId] );
191                $postChildren = $children[$postId];
192            }
193
194            // determine threading depth of post
195            $replyToId = $post->getReplyToId();
196            while ( $replyToId && isset( $children[$replyToId->getAlphadecimal()] ) ) {
197                $postDepth++;
198                $replyToId = $posts[$replyToId->getAlphadecimal()]->getReplyToId();
199            }
200
201            $post->setChildren( $postChildren );
202            $post->setDepth( $postDepth );
203        }
204
205        // return only the requested posts, rest are available as children.
206        // Return in same order as requested
207        /** @var PostRevision[] $roots */
208        $roots = [];
209        foreach ( $topicIds as $id ) {
210            $roots[$id->getAlphadecimal()] = $posts[$id->getAlphadecimal()];
211        }
212        // Attach every post in the tree to its root. setRootPost
213        // recursively applies it to all children as well.
214        foreach ( $roots as $post ) {
215            $post->setRootPost( $post );
216        }
217        return $roots;
218    }
219
220    /**
221     * @param UUID[] $postIds
222     * @return UUID[] Map from alphadecimal id to UUID object
223     */
224    protected function fetchRelatedPostIds( array $postIds ) {
225        // list of all posts descendant from the provided $postIds
226        $nodeList = $this->treeRepo->fetchSubtreeNodeList( $postIds );
227        // merge all the children from the various posts into one array
228        if ( !$nodeList ) {
229            // It should have returned at least $postIds
230            // TODO: log errors?
231            $res = $postIds;
232        } elseif ( count( $nodeList ) === 1 ) {
233            $res = reset( $nodeList );
234        } else {
235            $res = array_merge( ...array_values( $nodeList ) );
236        }
237
238        $retval = [];
239        foreach ( $res as $id ) {
240            $retval[$id->getAlphadecimal()] = $id;
241        }
242        return $retval;
243    }
244
245    /**
246     * @return TreeRepository
247     */
248    public function getTreeRepo() {
249        return $this->treeRepo;
250    }
251}