Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.71% covered (warning)
84.71%
72 / 85
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFlowThank
84.71% covered (warning)
84.71%
72 / 85
44.44% covered (danger)
44.44%
4 / 9
15.80
0.00% covered (danger)
0.00%
0 / 1
 execute
88.57% covered (warning)
88.57%
31 / 35
0.00% covered (danger)
0.00%
0 / 1
3.01
 userAlreadySentThanksForId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlowData
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 getRecipientFromPost
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getPageTitleFromRootPost
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTopicTitleFromRootPost
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sendThanks
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
2.00
 getAllowedParams
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Thanks\Api;
4
5use ApiBase;
6use Flow\Container;
7use Flow\Conversion\Utils;
8use Flow\Exception\FlowException;
9use Flow\Model\PostRevision;
10use Flow\Model\UUID;
11use MediaWiki\Extension\Notifications\Model\Event;
12use MediaWiki\Title\Title;
13use MediaWiki\User\User;
14use Wikimedia\ParamValidator\ParamValidator;
15
16/**
17 * API module to send Flow thanks notifications
18 *
19 * This API does not prevent sending thanks using post IDs that refer to topic
20 * titles, though Thank buttons are only shown for comments in the UI.
21 *
22 * @ingroup API
23 * @ingroup Extensions
24 */
25
26class ApiFlowThank extends ApiThank {
27
28    public function execute() {
29        $user = $this->getUser();
30        $this->dieOnBadUser( $user );
31        $this->dieOnUserBlockedFromThanks( $user );
32
33        $params = $this->extractRequestParams();
34
35        try {
36            $postId = UUID::create( $params['postid'] );
37        } catch ( FlowException $e ) {
38            $this->dieWithError( 'thanks-error-invalidpostid', 'invalidpostid' );
39        }
40
41        $data = $this->getFlowData( $postId );
42
43        $recipient = $this->getRecipientFromPost( $data['post'] );
44        $this->dieOnBadRecipient( $user, $recipient );
45
46        if ( $this->userAlreadySentThanksForId( $user, $postId ) ) {
47            $this->markResultSuccess( $recipient->getName() );
48            return;
49        }
50
51        $rootPost = $data['root'];
52        $workflowId = $rootPost->getPostId();
53        $rawTopicTitleText = Utils::htmlToPlaintext(
54            Container::get( 'templating' )->getContent( $rootPost, 'topic-title-html' )
55        );
56        // Truncate the title text to prevent issues with database storage.
57        $topicTitleText = $this->getLanguage()->truncateForDatabase( $rawTopicTitleText, 200 );
58        $pageTitle = $this->getPageTitleFromRootPost( $rootPost );
59        $this->dieOnUserBlockedFromTitle( $user, $pageTitle );
60
61        /** @var PostRevision $post */
62        $post = $data['post'];
63        $postText = Utils::htmlToPlaintext( $post->getContent() );
64        $postText = $this->getLanguage()->truncateForDatabase( $postText, 200 );
65
66        $topicTitle = $this->getTopicTitleFromRootPost( $rootPost );
67
68        $this->sendThanks(
69            $user,
70            $recipient,
71            $postId,
72            $workflowId,
73            $topicTitleText,
74            $pageTitle,
75            $postText,
76            $topicTitle
77        );
78    }
79
80    private function userAlreadySentThanksForId( User $user, UUID $id ) {
81        return $user->getRequest()->getSessionData( "flow-thanked-{$id->getAlphadecimal()}" );
82    }
83
84    /**
85     * @param UUID $postId UUID of the post to thank for
86     * @return array containing 'post' and 'root' as keys
87     */
88    private function getFlowData( UUID $postId ) {
89        $rootPostLoader = Container::get( 'loader.root_post' );
90        '@phan-var \Flow\Repository\RootPostLoader $rootPostLoader';
91
92        try {
93            $data = $rootPostLoader->getWithRoot( $postId );
94        } catch ( FlowException $e ) {
95            $this->dieWithError( 'thanks-error-invalidpostid', 'invalidpostid' );
96        }
97
98        if ( $data['post'] === null ) {
99            $this->dieWithError( 'thanks-error-invalidpostid', 'invalidpostid' );
100        }
101        // @phan-suppress-next-line PhanTypeMismatchReturnNullable T240141
102        return $data;
103    }
104
105    /**
106     * @param PostRevision $post
107     * @return User
108     */
109    private function getRecipientFromPost( PostRevision $post ) {
110        $recipient = User::newFromId( $post->getCreatorId() );
111        if ( !$recipient->loadFromId() ) {
112            $this->dieWithError( 'thanks-error-invalidrecipient', 'invalidrecipient' );
113        }
114        return $recipient;
115    }
116
117    /**
118     * @param PostRevision $rootPost
119     * @return Title
120     */
121    private function getPageTitleFromRootPost( PostRevision $rootPost ) {
122        $workflow = Container::get( 'storage' )->get( 'Workflow', $rootPost->getPostId() );
123        return $workflow->getOwnerTitle();
124    }
125
126    /**
127     * @param PostRevision $rootPost
128     * @return Title
129     */
130    private function getTopicTitleFromRootPost( PostRevision $rootPost ) {
131        $workflow = Container::get( 'storage' )->get( 'Workflow', $rootPost->getPostId() );
132        return $workflow->getArticleTitle();
133    }
134
135    /**
136     * @param User $user
137     * @param User $recipient
138     * @param UUID $postId
139     * @param UUID $workflowId
140     * @param string $topicTitleText
141     * @param Title $pageTitle
142     * @param string $postTextExcerpt
143     * @param Title $topicTitle
144     * @throws FlowException
145     */
146    private function sendThanks(
147        User $user,
148        User $recipient,
149        UUID $postId,
150        UUID $workflowId,
151        $topicTitleText,
152        Title $pageTitle,
153        $postTextExcerpt,
154        Title $topicTitle
155    ) {
156        $uniqueId = 'flow-' . $postId->getAlphadecimal();
157        // Do one last check to make sure we haven't sent Thanks before
158        if ( $this->haveAlreadyThanked( $user, $uniqueId ) ) {
159            // Pretend the thanks were sent
160            $this->markResultSuccess( $recipient->getName() );
161            return;
162        }
163
164        // Create the notification via Echo extension
165        Event::create( [
166            'type' => 'flow-thank',
167            'title' => $pageTitle,
168            'extra' => [
169                'post-id' => $postId->getAlphadecimal(),
170                'workflow' => $workflowId->getAlphadecimal(),
171                'thanked-user-id' => $recipient->getId(),
172                'topic-title' => $topicTitleText,
173                'excerpt' => $postTextExcerpt,
174                'target-page' => $topicTitle->getArticleID(),
175            ],
176            'agent' => $user,
177        ] );
178
179        // And mark the thank in session for a cheaper check to prevent duplicates (T48690).
180        $user->getRequest()->setSessionData( "flow-thanked-{$postId->getAlphadecimal()}", true );
181        // Set success message.
182        $this->markResultSuccess( $recipient->getName() );
183        $this->logThanks( $user, $recipient, $uniqueId );
184    }
185
186    public function getAllowedParams() {
187        return [
188            'postid' => [
189                ParamValidator::PARAM_TYPE => 'string',
190                ParamValidator::PARAM_REQUIRED => true,
191            ],
192            'token' => [
193                ParamValidator::PARAM_TYPE => 'string',
194                ParamValidator::PARAM_REQUIRED => true,
195            ],
196        ];
197    }
198
199    /**
200     * @see ApiBase::getExamplesMessages()
201     * @return array
202     */
203    protected function getExamplesMessages() {
204        return [
205            'action=flowthank&postid=xyz789&token=123ABC'
206                => 'apihelp-flowthank-example-1',
207        ];
208    }
209}