Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.63% covered (warning)
54.63%
59 / 108
53.85% covered (warning)
53.85%
14 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
PostRevision
54.63% covered (warning)
54.63%
59 / 108
53.85% covered (warning)
53.85%
14 / 26
253.31
0.00% covered (danger)
0.00%
0 / 1
 createTopicPost
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 newFromId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 fromStorageRow
47.06% covered (danger)
47.06%
8 / 17
0.00% covered (danger)
0.00%
0 / 1
8.71
 toStorageRow
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 reply
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getPostId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCreatorTuple
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTopicTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContentFormat
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getStorageFormat
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getWikitextFormat
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getHtmlFormat
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setReplyToId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReplyToId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setChildren
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getChildren
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDepth
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 setRootPost
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
4.12
 getRootPost
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
6.28
 getChildCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescendant
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getRevisionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCreator
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCollectionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Flow\Model;
4
5use Flow\Collection\PostCollection;
6use Flow\Container;
7use Flow\Exception\DataModelException;
8use Flow\Exception\FlowException;
9use Flow\Repository\TreeRepository;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12
13class 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}