Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.85% covered (warning)
53.85%
42 / 78
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageMessageBuilder
53.85% covered (warning)
53.85%
42 / 78
0.00% covered (danger)
0.00%
0 / 6
75.01
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getContent
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
6.04
 getContentWithFallback
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
6.04
 parseGetSectionResponse
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPageContent
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 isNotFoundError
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\MassMessage\PageMessage;
5
6use InvalidArgumentException;
7use MediaWiki\Languages\LanguageFallback;
8use MediaWiki\Languages\LanguageNameUtils;
9use MediaWiki\MassMessage\LanguageAwareText;
10use MediaWiki\MassMessage\MessageContentFetcher\LabeledSectionContentFetcher;
11use MediaWiki\MassMessage\MessageContentFetcher\LocalMessageContentFetcher;
12use MediaWiki\MassMessage\MessageContentFetcher\RemoteMessageContentFetcher;
13use MediaWiki\Status\Status;
14use MediaWiki\Title\Title;
15
16/**
17 * Contains logic to interact with page being sent as a mesasges
18 * @author Abijeet Patro
19 * @since 2022.01
20 * @license GPL-2.0-or-later
21 */
22class PageMessageBuilder {
23    /** @var string */
24    private $currentWikiId;
25    /** @var LocalMessageContentFetcher */
26    private $localMessageContentFetcher;
27    /** @var LabeledSectionContentFetcher */
28    private $labeledSectionContentFetcher;
29    /** @var RemoteMessageContentFetcher */
30    private $remoteMessageContentFetcher;
31    /** @var LanguageNameUtils */
32    private $languageNameUtils;
33    /** @var LanguageFallback */
34    private $languageFallback;
35
36    /**
37     * @param LocalMessageContentFetcher $localMessageContentFetcher
38     * @param LabeledSectionContentFetcher $labeledSectionContentFetcher
39     * @param RemoteMessageContentFetcher $remoteMessageContentFetcher
40     * @param LanguageNameUtils $languageNameUtils
41     * @param LanguageFallback $languageFallback
42     * @param string $currentWikiId
43     */
44    public function __construct(
45        LocalMessageContentFetcher $localMessageContentFetcher,
46        LabeledSectionContentFetcher $labeledSectionContentFetcher,
47        RemoteMessageContentFetcher $remoteMessageContentFetcher,
48        LanguageNameUtils $languageNameUtils,
49        LanguageFallback $languageFallback,
50        string $currentWikiId
51    ) {
52        $this->localMessageContentFetcher = $localMessageContentFetcher;
53        $this->labeledSectionContentFetcher = $labeledSectionContentFetcher;
54        $this->remoteMessageContentFetcher = $remoteMessageContentFetcher;
55        $this->languageNameUtils = $languageNameUtils;
56        $this->languageFallback = $languageFallback;
57        $this->currentWikiId = $currentWikiId;
58    }
59
60    /**
61     * Fetch content from a page or section of a page in a wiki to be used as the subject or
62     * in the message body for a MassMessage
63     *
64     * @param string $pageName
65     * @param string|null $pageMessageSection
66     * @param string|null $pageSubjectSection
67     * @param string $sourceWikiId
68     * @return PageMessageBuilderResult
69     */
70    public function getContent(
71        string $pageName,
72        ?string $pageMessageSection,
73        ?string $pageSubjectSection,
74        string $sourceWikiId
75    ): PageMessageBuilderResult {
76        if ( $pageName === '' ) {
77            throw new InvalidArgumentException( 'Empty page name passed' );
78        }
79
80        $pageContentStatus = $this->getPageContent( $pageName, $sourceWikiId );
81        if ( !$pageContentStatus->isOK() ) {
82            return new PageMessageBuilderResult( $pageContentStatus );
83        }
84
85        /** @var LanguageAwareText */
86        $pageContent = $pageContentStatus->getValue();
87        if ( $pageContent->getWikitext() === '' ) {
88            return new PageMessageBuilderResult( Status::newFatal( 'massmessage-page-message-empty', $pageName ) );
89        }
90
91        $pageMessage = $pageContent;
92        $pageSubject = null;
93        $finalStatus = $pageContentStatus;
94
95        if ( $pageMessageSection ) {
96            // Include section tags for backwards compatibility.
97            // https://phabricator.wikimedia.org/T254481#6865334
98            $messageSectionStatus = $this->labeledSectionContentFetcher
99                ->getContent( $pageContent, $pageMessageSection );
100            $pageMessage = $this->parseGetSectionResponse(
101                $messageSectionStatus,
102                $finalStatus,
103                Status::newFatal( 'massmessage-page-message-empty', $pageName )
104            );
105        }
106
107        if ( $pageSubjectSection ) {
108            $subjectSectionStatus = $this->labeledSectionContentFetcher
109                ->getContentWithoutTags( $pageContent, $pageSubjectSection );
110            $pageSubject = $this->parseGetSectionResponse(
111                $subjectSectionStatus,
112                $finalStatus,
113                Status::newFatal( 'massmessage-page-subject-empty', $pageSubjectSection, $pageName )
114            );
115        }
116
117        return new PageMessageBuilderResult( $finalStatus, $pageMessage, $pageSubject );
118    }
119
120    /**
121     * Get content for a target language from wiki, using fallbacks if necessary
122     *
123     * @param string $titleStr
124     * @param string $targetLangCode
125     * @param string $sourceLangCode
126     * @param string|null $pageMessageSection
127     * @param string|null $pageSubjectSection
128     * @param string $sourceWikiId
129     * @return PageMessageBuilderResult Values is LanguageAwareText or null on failure
130     */
131    public function getContentWithFallback(
132        string $titleStr,
133        string $targetLangCode,
134        string $sourceLangCode,
135        ?string $pageMessageSection,
136        ?string $pageSubjectSection,
137        string $sourceWikiId
138    ): PageMessageBuilderResult {
139        if ( !$this->languageNameUtils->isKnownLanguageTag( $targetLangCode ) ) {
140            return new PageMessageBuilderResult( Status::newFatal( 'massmessage-invalid-lang', $targetLangCode ) );
141        }
142
143        // Identify languages to fetch
144        $fallbackChain = array_merge(
145            [ $targetLangCode ],
146            $this->languageFallback->getAll( $targetLangCode )
147        );
148
149        foreach ( $fallbackChain as $langCode ) {
150            $titleStrWithLang = $titleStr . '/' . $langCode;
151            $pageMessageBuilderResult = $this->getContent(
152                $titleStrWithLang, $pageMessageSection, $pageSubjectSection, $sourceWikiId
153            );
154
155            if ( $pageMessageBuilderResult->isOK() ) {
156                return $pageMessageBuilderResult;
157            }
158
159            // Got an unknown error, let's stop looking for other fallbacks
160            if ( !$this->isNotFoundError( $pageMessageBuilderResult->getStatus() ) ) {
161                break;
162            }
163        }
164
165        // No language or fallback found or there was an error, go with source language
166        $langSuffix = '';
167        if ( $sourceLangCode ) {
168            $langSuffix = "/$sourceLangCode";
169        }
170
171        return $this->getContent( $titleStr . $langSuffix, $pageMessageSection, $pageSubjectSection, $sourceWikiId );
172    }
173
174    /**
175     * Helper method to parse response from get labeled section method and updates the passed status
176     *
177     * @param Status $sectionStatus Status from get labeled section
178     * @param Status $statusToUpdate Status to update
179     * @param Status $emptySectionErrorStatus Fatal status to use if section content is empty
180     * @return LanguageAwareText|null
181     */
182    private function parseGetSectionResponse(
183        Status $sectionStatus,
184        Status $statusToUpdate,
185        Status $emptySectionErrorStatus
186    ): ?LanguageAwareText {
187        if ( !$sectionStatus->isOK() ) {
188            $statusToUpdate = $statusToUpdate->merge( $sectionStatus );
189        } else {
190            /** @var LanguageAwareText */
191            $sectionContent = $sectionStatus->getValue();
192            if ( $sectionContent->getWikitext() === '' ) {
193                $statusToUpdate->merge( $emptySectionErrorStatus );
194            } else {
195                return $sectionContent;
196            }
197        }
198        return null;
199    }
200
201    /**
202     * Uses the database or API to fetch content based on the wiki.
203     *
204     * @param string $titleStr
205     * @param string $wikiId
206     * @return Status Values is LanguageAwareText or null on failure
207     */
208    private function getPageContent( string $titleStr, string $wikiId ): Status {
209        $isCurrentWiki = $this->currentWikiId === $wikiId;
210        $title = Title::newFromText( $titleStr );
211        if ( $title === null ) {
212            return Status::newFatal(
213                'massmessage-page-message-invalid', $titleStr
214            );
215        }
216
217        if ( $isCurrentWiki ) {
218            return $this->localMessageContentFetcher->getContent( $title );
219        }
220
221        return $this->remoteMessageContentFetcher->getContent( $titleStr, $wikiId );
222    }
223
224    /**
225     * Checks if a given Status is a not found error.
226     *
227     * @param Status $status
228     * @return bool
229     */
230    private function isNotFoundError( Status $status ): bool {
231        $notFoundErrors = [
232            'massmessage-page-message-not-found', 'massmessage-page-message-not-found-in-wiki'
233        ];
234        $errors = $status->getErrors();
235        if ( $errors ) {
236            foreach ( $errors as $error ) {
237                if ( in_array( $error['message'], $notFoundErrors ) ) {
238                    return true;
239                }
240            }
241        }
242
243        return false;
244    }
245}