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