Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
56.48% |
61 / 108 |
|
53.85% |
14 / 26 |
CRAP | |
0.00% |
0 / 1 |
PostRevision | |
56.48% |
61 / 108 |
|
53.85% |
14 / 26 |
229.06 | |
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 | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
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). |
90 | * |
91 | * @param UUID $uuid |
92 | * @param User $user |
93 | * @param string $content |
94 | * @param string $format wikitext|html |
95 | * @param Title|null $title |
96 | * @return PostRevision |
97 | */ |
98 | public static function newFromId( UUID $uuid, User $user, $content, $format, ?Title $title = null ) { |
99 | $obj = new self; |
100 | $obj->revId = UUID::create(); |
101 | $obj->postId = $uuid; |
102 | |
103 | $obj->user = UserTuple::newFromUser( $user ); |
104 | $obj->origUser = $obj->user; |
105 | |
106 | $obj->setReplyToId( null ); // not a reply to anything |
107 | $obj->prevRevision = null; // no parent revision |
108 | $obj->setContent( $content, $format, $title ); |
109 | |
110 | return $obj; |
111 | } |
112 | |
113 | /** |
114 | * @param string[] $row |
115 | * @param PostRevision|null $obj |
116 | * @return PostRevision |
117 | * @throws DataModelException |
118 | */ |
119 | public static function fromStorageRow( array $row, $obj = null ) { |
120 | /** @var PostRevision $obj */ |
121 | $obj = parent::fromStorageRow( $row, $obj ); |
122 | '@phan-var PostRevision $obj'; |
123 | $treeRevId = UUID::create( $row['tree_rev_id'] ); |
124 | |
125 | if ( !$obj->revId->equals( $treeRevId ) ) { |
126 | $treeRevIdStr = ( $treeRevId !== null ) |
127 | ? $treeRevId->getAlphadecimal() |
128 | : var_export( $row['tree_rev_id'], true ); |
129 | |
130 | throw new DataModelException( |
131 | 'tree revision doesn\'t match provided revision: treeRevId (' |
132 | . $treeRevIdStr . ') != obj->revId (' . $obj->revId->getAlphadecimal() . ')', |
133 | 'process-data' |
134 | ); |
135 | } |
136 | $obj->replyToId = $row['tree_parent_id'] ? UUID::create( $row['tree_parent_id'] ) : null; |
137 | $obj->postId = UUID::create( $row['rev_type_id'] ); |
138 | $obj->origUser = UserTuple::newFromArray( $row, 'tree_orig_user_' ); |
139 | if ( !$obj->origUser ) { |
140 | throw new DataModelException( 'Could not create UserTuple for tree_orig_user_' ); |
141 | } |
142 | return $obj; |
143 | } |
144 | |
145 | /** |
146 | * @param PostRevision $rev |
147 | * @return string[] |
148 | * @suppress PhanParamSignatureMismatch It doesn't match |
149 | */ |
150 | public static function toStorageRow( $rev ) { |
151 | return parent::toStorageRow( $rev ) + [ |
152 | 'tree_parent_id' => $rev->replyToId ? $rev->replyToId->getAlphadecimal() : null, |
153 | 'tree_rev_descendant_id' => $rev->postId->getAlphadecimal(), |
154 | 'tree_rev_id' => $rev->revId->getAlphadecimal(), |
155 | // rest of tree_ is denormalized data about first post revision |
156 | 'tree_orig_user_id' => $rev->origUser->id, |
157 | 'tree_orig_user_ip' => $rev->origUser->ip, |
158 | 'tree_orig_user_wiki' => $rev->origUser->wiki, |
159 | ]; |
160 | } |
161 | |
162 | /** |
163 | * @param Workflow $workflow |
164 | * @param User $user |
165 | * @param string $content |
166 | * @param string $format wikitext|html |
167 | * @param string $changeType |
168 | * @return PostRevision |
169 | */ |
170 | public function reply( Workflow $workflow, User $user, $content, $format, $changeType = 'reply' ) { |
171 | $reply = new self; |
172 | |
173 | // UUIDs should not be reused for different entities/entity types in the future. |
174 | // (It is also inconsistent with newFromId, which uses separate ones.) |
175 | // This may be changed here in the future. |
176 | $reply->revId = $reply->postId = UUID::create(); |
177 | |
178 | $reply->user = UserTuple::newFromUser( $user ); |
179 | $reply->origUser = $reply->user; |
180 | $reply->replyToId = $this->postId; |
181 | $reply->setContent( $content, $format, $workflow->getArticleTitle() ); |
182 | $reply->changeType = $changeType; |
183 | $reply->setChildren( [] ); |
184 | $reply->setDepth( $this->getDepth() + 1 ); |
185 | $reply->rootPost = $this->rootPost; |
186 | |
187 | return $reply; |
188 | } |
189 | |
190 | /** |
191 | * @return UUID |
192 | */ |
193 | public function getPostId() { |
194 | return $this->postId; |
195 | } |
196 | |
197 | /** |
198 | * @return UserTuple |
199 | */ |
200 | public function getCreatorTuple() { |
201 | return $this->origUser; |
202 | } |
203 | |
204 | /** |
205 | * @return bool |
206 | */ |
207 | public function isTopicTitle() { |
208 | return $this->replyToId === null; |
209 | } |
210 | |
211 | public function getContentFormat() { |
212 | // The canonical format must always be topic-title-wikitext, because we |
213 | // can not convert 'topic-title-html' to 'topic-title-wikitext'. |
214 | if ( $this->isTopicTitle() ) { |
215 | return 'topic-title-wikitext'; |
216 | } else { |
217 | return parent::getContentFormat(); |
218 | } |
219 | } |
220 | |
221 | /** |
222 | * Gets the desired storage format. |
223 | * |
224 | * @return string |
225 | */ |
226 | protected function getStorageFormat() { |
227 | if ( $this->isTopicTitle() ) { |
228 | return 'topic-title-wikitext'; |
229 | } else { |
230 | return parent::getStorageFormat(); |
231 | } |
232 | } |
233 | |
234 | /** |
235 | * Gets the appropriate wikitext format string for this revision. |
236 | * |
237 | * @return string 'wikitext' or 'topic-title-wikitext' |
238 | */ |
239 | public function getWikitextFormat() { |
240 | if ( $this->isTopicTitle() ) { |
241 | return 'topic-title-wikitext'; |
242 | } else { |
243 | return parent::getWikitextFormat(); |
244 | } |
245 | } |
246 | |
247 | /** |
248 | * Gets the appropriate HTML format string for this revision. |
249 | * |
250 | * @return string 'html' or 'topic-title-html' |
251 | */ |
252 | public function getHtmlFormat() { |
253 | if ( $this->isTopicTitle() ) { |
254 | return 'topic-title-html'; |
255 | } else { |
256 | return parent::getHtmlFormat(); |
257 | } |
258 | } |
259 | |
260 | /** |
261 | * @param UUID|null $id |
262 | */ |
263 | public function setReplyToId( ?UUID $id = null ) { |
264 | $this->replyToId = $id; |
265 | } |
266 | |
267 | /** |
268 | * @return UUID|null Id of the parent post, or null if this is the root |
269 | */ |
270 | public function getReplyToId() { |
271 | return $this->replyToId; |
272 | } |
273 | |
274 | /** |
275 | * @param PostRevision[] $children |
276 | */ |
277 | public function setChildren( array $children ) { |
278 | $this->children = $children; |
279 | if ( $this->rootPost ) { |
280 | // Propagate root post into children. |
281 | $this->setRootPost( $this->rootPost ); |
282 | } |
283 | } |
284 | |
285 | /** |
286 | * @return PostRevision[] |
287 | * @throws DataModelException |
288 | */ |
289 | public function getChildren() { |
290 | if ( $this->children === null ) { |
291 | throw new DataModelException( 'Children not loaded for post: ' . $this->postId->getAlphadecimal(), 'process-data' ); |
292 | } |
293 | return $this->children; |
294 | } |
295 | |
296 | /** |
297 | * @param int $depth |
298 | */ |
299 | public function setDepth( $depth ) { |
300 | $this->depth = (int)$depth; |
301 | } |
302 | |
303 | /** |
304 | * @return int |
305 | * @throws DataModelException |
306 | */ |
307 | public function getDepth() { |
308 | if ( $this->depth === null ) { |
309 | /** @var TreeRepository $treeRepo */ |
310 | $treeRepo = Container::get( 'repository.tree' ); |
311 | $rootPath = $treeRepo->findRootPath( $this->getCollectionId() ); |
312 | $this->setDepth( count( $rootPath ) - 1 ); |
313 | } |
314 | |
315 | return $this->depth; |
316 | } |
317 | |
318 | /** |
319 | * @param PostRevision $root |
320 | * @deprecated Use PostCollection::getRoot instead |
321 | */ |
322 | public function setRootPost( PostRevision $root ) { |
323 | $this->rootPost = $root; |
324 | if ( $this->children ) { |
325 | // Propagate root post into children. |
326 | foreach ( $this->children as $child ) { |
327 | $child->setRootPost( $root ); |
328 | } |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * @return PostRevision |
334 | * @throws DataModelException |
335 | * @deprecated Use PostCollection::getRoot instead |
336 | */ |
337 | public function getRootPost() { |
338 | if ( $this->isTopicTitle() ) { |
339 | return $this; |
340 | } elseif ( $this->rootPost === null ) { |
341 | $collection = $this->getCollection(); |
342 | $root = $collection->getRoot(); |
343 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
344 | return $root->getLastRevision(); |
345 | } |
346 | return $this->rootPost; |
347 | } |
348 | |
349 | /** |
350 | * Get the amount of posts in this topic. |
351 | * |
352 | * @return int |
353 | */ |
354 | public function getChildCount() { |
355 | return count( $this->getChildren() ); |
356 | } |
357 | |
358 | /** |
359 | * Finds the provided postId within this posts descendants |
360 | * |
361 | * @param UUID $postId The id of the post to find. |
362 | * @return PostRevision|null |
363 | * @throws FlowException |
364 | */ |
365 | public function getDescendant( UUID $postId ) { |
366 | if ( $this->children === null ) { |
367 | throw new FlowException( 'Attempted to access post descendant, but children haven\'t yet been loaded.' ); |
368 | } |
369 | foreach ( $this->children as $child ) { |
370 | if ( $child->getPostId()->equals( $postId ) ) { |
371 | return $child; |
372 | } |
373 | $found = $child->getDescendant( $postId ); |
374 | if ( $found !== null ) { |
375 | return $found; |
376 | } |
377 | } |
378 | |
379 | return null; |
380 | } |
381 | |
382 | /** |
383 | * @return string |
384 | */ |
385 | public function getRevisionType() { |
386 | return 'post'; |
387 | } |
388 | |
389 | /** |
390 | * @param User $user |
391 | * @return bool |
392 | */ |
393 | public function isCreator( User $user ) { |
394 | if ( !$user->isRegistered() ) { |
395 | return false; |
396 | } |
397 | return $user->getId() == $this->getCreatorId(); |
398 | } |
399 | |
400 | /** |
401 | * @return UUID |
402 | */ |
403 | public function getCollectionId() { |
404 | return $this->getPostId(); |
405 | } |
406 | |
407 | /** |
408 | * @return PostCollection |
409 | */ |
410 | public function getCollection() { |
411 | // @phan-suppress-next-line PhanTypeMismatchReturnSuperType |
412 | return PostCollection::newFromRevision( $this ); |
413 | } |
414 | } |