Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.48% covered (warning)
56.48%
61 / 108
53.85% covered (warning)
53.85%
14 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
PostRevision
56.48% covered (warning)
56.48%
61 / 108
53.85% covered (warning)
53.85%
14 / 26
229.06
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
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 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).
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}