Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
54.63% |
59 / 108 |
|
53.85% |
14 / 26 |
CRAP | |
0.00% |
0 / 1 |
PostRevision | |
54.63% |
59 / 108 |
|
53.85% |
14 / 26 |
253.31 | |
0.00% |
0 / 1 |
createTopicPost | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
newFromId | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
fromStorageRow | |
47.06% |
8 / 17 |
|
0.00% |
0 / 1 |
8.71 | |||
toStorageRow | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
reply | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getPostId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCreatorTuple | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isTopicTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentFormat | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getStorageFormat | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getWikitextFormat | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getHtmlFormat | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setReplyToId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReplyToId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setChildren | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getChildren | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setDepth | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDepth | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
2.86 | |||
setRootPost | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
getRootPost | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
6.28 | |||
getChildCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescendant | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
getRevisionType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isCreator | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getCollectionId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCollection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace Flow\Model; |
4 | |
5 | use Flow\Collection\PostCollection; |
6 | use Flow\Container; |
7 | use Flow\Exception\DataModelException; |
8 | use Flow\Exception\FlowException; |
9 | use Flow\Repository\TreeRepository; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\User\User; |
12 | |
13 | class PostRevision extends AbstractRevision { |
14 | public const MAX_TOPIC_LENGTH = 260; |
15 | |
16 | /** |
17 | * @var UUID |
18 | */ |
19 | protected $postId; |
20 | |
21 | // The rest of the properties are denormalized data that |
22 | // must not change between revisions of same post |
23 | |
24 | /** |
25 | * @var UserTuple |
26 | */ |
27 | protected $origUser; |
28 | |
29 | /** |
30 | * @var UUID|null |
31 | */ |
32 | protected $replyToId; |
33 | |
34 | /** |
35 | * @var PostRevision[]|null Optionally loaded list of children for this post. |
36 | */ |
37 | protected $children; |
38 | |
39 | /** |
40 | * @var int|null Optionally loaded distance of this post from the |
41 | * root of this post tree. |
42 | */ |
43 | protected $depth; |
44 | |
45 | /** |
46 | * @var PostRevision|null Optionally loaded root of this posts tree. |
47 | * This is always a topic title. |
48 | */ |
49 | protected $rootPost; |
50 | |
51 | /** |
52 | * Create a brand new root post for a brand new topic. Creating replies to |
53 | * an existing post(incl topic root) should use self::reply. |
54 | * |
55 | * @param Workflow $topic |
56 | * @param User $user |
57 | * @param string $content The title of the topic (they are Collection as well), in |
58 | * topic-title-wikitext format. |
59 | * @return PostRevision |
60 | */ |
61 | public static function createTopicPost( Workflow $topic, User $user, $content ) { |
62 | $format = 'topic-title-wikitext'; |
63 | $obj = static::newFromId( $topic->getId(), $user, $content, $format, $topic->getArticleTitle() ); |
64 | |
65 | $obj->changeType = 'new-post'; |
66 | // A newly created post has no children, a depth of 0, and |
67 | // is the root of its tree. |
68 | $obj->setChildren( [] ); |
69 | $obj->setDepth( 0 ); |
70 | $obj->rootPost = $obj; |
71 | |
72 | return $obj; |
73 | } |
74 | |
75 | /** |
76 | * DO NOT USE THIS METHOD! |
77 | * |
78 | * Seriously, you probably don't want to use this method, except from within |
79 | * this class. |
80 | * |
81 | * Although it may seem similar to Title::newFrom* or User::newFrom*, chances are slim to none |
82 | * that this will do what you'd expect. |
83 | * |
84 | * Unlike Title & User etc, a post is not something some object that can be |
85 | * used in isolation: a post should always be retrieved via it's parents, |
86 | * via a workflow, ... |
87 | * |
88 | * The only reasons we have this method are for creating root posts |
89 | * (called from PostRevision->create), and so when failing to load a |
90 | * post, we can create a stub object. |
91 | * |
92 | * @param UUID $uuid |
93 | * @param User $user |
94 | * @param string $content |
95 | * @param string $format wikitext|html |
96 | * @param Title|null $title |
97 | * @return PostRevision |
98 | */ |
99 | public static function newFromId( UUID $uuid, User $user, $content, $format, Title $title = null ) { |
100 | $obj = new self; |
101 | $obj->revId = UUID::create(); |
102 | $obj->postId = $uuid; |
103 | |
104 | $obj->user = UserTuple::newFromUser( $user ); |
105 | $obj->origUser = $obj->user; |
106 | |
107 | $obj->setReplyToId( null ); // not a reply to anything |
108 | $obj->prevRevision = null; // no parent revision |
109 | $obj->setContent( $content, $format, $title ); |
110 | |
111 | return $obj; |
112 | } |
113 | |
114 | /** |
115 | * @param string[] $row |
116 | * @param PostRevision|null $obj |
117 | * @return PostRevision |
118 | * @throws DataModelException |
119 | */ |
120 | public static function fromStorageRow( array $row, $obj = null ) { |
121 | /** @var PostRevision $obj */ |
122 | $obj = parent::fromStorageRow( $row, $obj ); |
123 | '@phan-var PostRevision $obj'; |
124 | $treeRevId = UUID::create( $row['tree_rev_id'] ); |
125 | |
126 | if ( !$obj->revId->equals( $treeRevId ) ) { |
127 | $treeRevIdStr = ( $treeRevId !== null ) |
128 | ? $treeRevId->getAlphadecimal() |
129 | : var_export( $row['tree_rev_id'], true ); |
130 | |
131 | throw new DataModelException( |
132 | 'tree revision doesn\'t match provided revision: treeRevId (' |
133 | . $treeRevIdStr . ') != obj->revId (' . $obj->revId->getAlphadecimal() . ')', |
134 | 'process-data' |
135 | ); |
136 | } |
137 | $obj->replyToId = $row['tree_parent_id'] ? UUID::create( $row['tree_parent_id'] ) : null; |
138 | $obj->postId = UUID::create( $row['rev_type_id'] ); |
139 | $obj->origUser = UserTuple::newFromArray( $row, 'tree_orig_user_' ); |
140 | if ( !$obj->origUser ) { |
141 | throw new DataModelException( 'Could not create UserTuple for tree_orig_user_' ); |
142 | } |
143 | return $obj; |
144 | } |
145 | |
146 | /** |
147 | * @param PostRevision $rev |
148 | * @return string[] |
149 | * @suppress PhanParamSignatureMismatch It doesn't match |
150 | */ |
151 | public static function toStorageRow( $rev ) { |
152 | return parent::toStorageRow( $rev ) + [ |
153 | 'tree_parent_id' => $rev->replyToId ? $rev->replyToId->getAlphadecimal() : null, |
154 | 'tree_rev_descendant_id' => $rev->postId->getAlphadecimal(), |
155 | 'tree_rev_id' => $rev->revId->getAlphadecimal(), |
156 | // rest of tree_ is denormalized data about first post revision |
157 | 'tree_orig_user_id' => $rev->origUser->id, |
158 | 'tree_orig_user_ip' => $rev->origUser->ip, |
159 | 'tree_orig_user_wiki' => $rev->origUser->wiki, |
160 | ]; |
161 | } |
162 | |
163 | /** |
164 | * @param Workflow $workflow |
165 | * @param User $user |
166 | * @param string $content |
167 | * @param string $format wikitext|html |
168 | * @param string $changeType |
169 | * @return PostRevision |
170 | */ |
171 | public function reply( Workflow $workflow, User $user, $content, $format, $changeType = 'reply' ) { |
172 | $reply = new self; |
173 | |
174 | // UUIDs should not be reused for different entities/entity types in the future. |
175 | // (It is also inconsistent with newFromId, which uses separate ones.) |
176 | // This may be changed here in the future. |
177 | $reply->revId = $reply->postId = UUID::create(); |
178 | |
179 | $reply->user = UserTuple::newFromUser( $user ); |
180 | $reply->origUser = $reply->user; |
181 | $reply->replyToId = $this->postId; |
182 | $reply->setContent( $content, $format, $workflow->getArticleTitle() ); |
183 | $reply->changeType = $changeType; |
184 | $reply->setChildren( [] ); |
185 | $reply->setDepth( $this->getDepth() + 1 ); |
186 | $reply->rootPost = $this->rootPost; |
187 | |
188 | return $reply; |
189 | } |
190 | |
191 | /** |
192 | * @return UUID |
193 | */ |
194 | public function getPostId() { |
195 | return $this->postId; |
196 | } |
197 | |
198 | /** |
199 | * @return UserTuple |
200 | */ |
201 | public function getCreatorTuple() { |
202 | return $this->origUser; |
203 | } |
204 | |
205 | /** |
206 | * @return bool |
207 | */ |
208 | public function isTopicTitle() { |
209 | return $this->replyToId === null; |
210 | } |
211 | |
212 | public function getContentFormat() { |
213 | // The canonical format must always be topic-title-wikitext, because we |
214 | // can not convert 'topic-title-html' to 'topic-title-wikitext'. |
215 | if ( $this->isTopicTitle() ) { |
216 | return 'topic-title-wikitext'; |
217 | } else { |
218 | return parent::getContentFormat(); |
219 | } |
220 | } |
221 | |
222 | /** |
223 | * Gets the desired storage format. |
224 | * |
225 | * @return string |
226 | */ |
227 | protected function getStorageFormat() { |
228 | if ( $this->isTopicTitle() ) { |
229 | return 'topic-title-wikitext'; |
230 | } else { |
231 | return parent::getStorageFormat(); |
232 | } |
233 | } |
234 | |
235 | /** |
236 | * Gets the appropriate wikitext format string for this revision. |
237 | * |
238 | * @return string 'wikitext' or 'topic-title-wikitext' |
239 | */ |
240 | public function getWikitextFormat() { |
241 | if ( $this->isTopicTitle() ) { |
242 | return 'topic-title-wikitext'; |
243 | } else { |
244 | return parent::getWikitextFormat(); |
245 | } |
246 | } |
247 | |
248 | /** |
249 | * Gets the appropriate HTML format string for this revision. |
250 | * |
251 | * @return string 'html' or 'topic-title-html' |
252 | */ |
253 | public function getHtmlFormat() { |
254 | if ( $this->isTopicTitle() ) { |
255 | return 'topic-title-html'; |
256 | } else { |
257 | return parent::getHtmlFormat(); |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * @param UUID|null $id |
263 | */ |
264 | public function setReplyToId( UUID $id = null ) { |
265 | $this->replyToId = $id; |
266 | } |
267 | |
268 | /** |
269 | * @return UUID|null Id of the parent post, or null if this is the root |
270 | */ |
271 | public function getReplyToId() { |
272 | return $this->replyToId; |
273 | } |
274 | |
275 | /** |
276 | * @param PostRevision[] $children |
277 | */ |
278 | public function setChildren( array $children ) { |
279 | $this->children = $children; |
280 | if ( $this->rootPost ) { |
281 | // Propagate root post into children. |
282 | $this->setRootPost( $this->rootPost ); |
283 | } |
284 | } |
285 | |
286 | /** |
287 | * @return PostRevision[] |
288 | * @throws DataModelException |
289 | */ |
290 | public function getChildren() { |
291 | if ( $this->children === null ) { |
292 | throw new DataModelException( 'Children not loaded for post: ' . $this->postId->getAlphadecimal(), 'process-data' ); |
293 | } |
294 | return $this->children; |
295 | } |
296 | |
297 | /** |
298 | * @param int $depth |
299 | */ |
300 | public function setDepth( $depth ) { |
301 | $this->depth = (int)$depth; |
302 | } |
303 | |
304 | /** |
305 | * @return int |
306 | * @throws DataModelException |
307 | */ |
308 | public function getDepth() { |
309 | if ( $this->depth === null ) { |
310 | /** @var TreeRepository $treeRepo */ |
311 | $treeRepo = Container::get( 'repository.tree' ); |
312 | $rootPath = $treeRepo->findRootPath( $this->getCollectionId() ); |
313 | $this->setDepth( count( $rootPath ) - 1 ); |
314 | } |
315 | |
316 | return $this->depth; |
317 | } |
318 | |
319 | /** |
320 | * @param PostRevision $root |
321 | * @deprecated Use PostCollection::getRoot instead |
322 | */ |
323 | public function setRootPost( PostRevision $root ) { |
324 | $this->rootPost = $root; |
325 | if ( $this->children ) { |
326 | // Propagate root post into children. |
327 | foreach ( $this->children as $child ) { |
328 | $child->setRootPost( $root ); |
329 | } |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * @return PostRevision |
335 | * @throws DataModelException |
336 | * @deprecated Use PostCollection::getRoot instead |
337 | */ |
338 | public function getRootPost() { |
339 | if ( $this->isTopicTitle() ) { |
340 | return $this; |
341 | } elseif ( $this->rootPost === null ) { |
342 | $collection = $this->getCollection(); |
343 | $root = $collection->getRoot(); |
344 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
345 | return $root->getLastRevision(); |
346 | } |
347 | return $this->rootPost; |
348 | } |
349 | |
350 | /** |
351 | * Get the amount of posts in this topic. |
352 | * |
353 | * @return int |
354 | */ |
355 | public function getChildCount() { |
356 | return count( $this->getChildren() ); |
357 | } |
358 | |
359 | /** |
360 | * Finds the provided postId within this posts descendants |
361 | * |
362 | * @param UUID $postId The id of the post to find. |
363 | * @return PostRevision|null |
364 | * @throws FlowException |
365 | */ |
366 | public function getDescendant( UUID $postId ) { |
367 | if ( $this->children === null ) { |
368 | throw new FlowException( 'Attempted to access post descendant, but children haven\'t yet been loaded.' ); |
369 | } |
370 | foreach ( $this->children as $child ) { |
371 | if ( $child->getPostId()->equals( $postId ) ) { |
372 | return $child; |
373 | } |
374 | $found = $child->getDescendant( $postId ); |
375 | if ( $found !== null ) { |
376 | return $found; |
377 | } |
378 | } |
379 | |
380 | return null; |
381 | } |
382 | |
383 | /** |
384 | * @return string |
385 | */ |
386 | public function getRevisionType() { |
387 | return 'post'; |
388 | } |
389 | |
390 | /** |
391 | * @param User $user |
392 | * @return bool |
393 | */ |
394 | public function isCreator( User $user ) { |
395 | if ( !$user->isRegistered() ) { |
396 | return false; |
397 | } |
398 | return $user->getId() == $this->getCreatorId(); |
399 | } |
400 | |
401 | /** |
402 | * @return UUID |
403 | */ |
404 | public function getCollectionId() { |
405 | return $this->getPostId(); |
406 | } |
407 | |
408 | /** |
409 | * @return PostCollection |
410 | */ |
411 | public function getCollection() { |
412 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
413 | return PostCollection::newFromRevision( $this ); |
414 | } |
415 | } |