Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
BannerMessage
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 8
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDbKey
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 existsInLang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContents
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 toHtml
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 update
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 sanitize
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\Content\ContentHandler;
4use MediaWiki\Content\TextContent;
5use MediaWiki\Context\IContextSource;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Parser\Sanitizer;
8use MediaWiki\Revision\SlotRecord;
9use MediaWiki\Title\Title;
10use MediaWiki\User\User;
11use Wikimedia\Rdbms\IDBAccessObject;
12
13class BannerMessage {
14
15    /** @var string */
16    private $banner_name;
17    /** @var string */
18    private $name;
19
20    private const SPAN_TAG_PLACEHOLDER_START = '%%%spantagplaceholderstart%%%';
21    private const SPAN_TAG_PLACEHOLDER_END = '%%%spantagplaceholderend%%%';
22
23    public function __construct( $banner_name, $name ) {
24        $this->banner_name = $banner_name;
25        $this->name = $name;
26    }
27
28    public function getTitle( $lang, $namespace = NS_MEDIAWIKI ) {
29        return Title::newFromText( $this->getDbKey( $lang, $namespace ), $namespace );
30    }
31
32    /**
33     * Obtains the key of the message as stored in the database. This varies depending on namespace
34     *  - in the MediaWiki namespace messages are Centralnotice-{banner name}-{message name}/{lang}
35     *  -- except for the content language which is stored without the /{lang} extension
36     *  - in the CN Banner namespace messages are {banner name}-{message name}/{lang}
37     *
38     * @param string|null $lang Language code
39     * @param int $namespace Namespace to get key for
40     *
41     * @return string Message database key
42     * @throws RangeException
43     */
44    public function getDbKey( $lang = null, $namespace = NS_MEDIAWIKI ) {
45        global $wgLanguageCode;
46
47        if ( $namespace === NS_MEDIAWIKI ) {
48            return ( $lang === null || $lang === $wgLanguageCode ) ?
49                "Centralnotice-{$this->banner_name}-{$this->name}" :
50                "Centralnotice-{$this->banner_name}-{$this->name}/{$lang}";
51        } elseif ( $namespace === NS_CN_BANNER ) {
52            return "{$this->banner_name}-{$this->name}/{$lang}";
53        } else {
54            throw new RangeException(
55                "Namespace '$namespace' not known for having CentralNotice messages."
56            );
57        }
58    }
59
60    /**
61     * Return the whether the message exists, without language fallback.
62     * @param string|null $lang
63     * @return bool
64     */
65    public function existsInLang( $lang ) {
66        return $this->getTitle( $lang )->exists();
67    }
68
69    /**
70     * Obtain the raw contents of the message; stripping out the stupid <message-name> if it's blank
71     *
72     * @param null|string $lang
73     * @return null|string Will be null if the message does not exist, otherwise will be
74     * the contents of the message.
75     */
76    public function getContents( $lang ) {
77        if ( $this->existsInLang( $lang ) ) {
78            $dbKey = $this->getDbKey();
79            $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
80            $rev = $revisionLookup->getRevisionByTitle( $this->getTitle( $lang ) );
81
82            if ( !$rev ) {
83                // Try harder, might have just been created, otherwise the title wouldn't exist
84                $rev = $revisionLookup->getRevisionByTitle(
85                    $this->getTitle( $lang ),
86                    IDBAccessObject::READ_LATEST
87                );
88            }
89
90            if ( !$rev ) {
91                return null;
92            }
93
94            $content = $rev->getContent( SlotRecord::MAIN );
95            /** @var TextContent $content */
96            '@phan-var TextContent $content';
97            $msg = $content->getText();
98            if ( $msg === "&lt;{$dbKey}&gt;" ) {
99                $msg = '';
100            }
101            return $msg;
102        } else {
103            return null;
104        }
105    }
106
107    public function toHtml( IContextSource $context ) {
108        global $wgNoticeUseLanguageConversion;
109        $lang = $context->getLanguage();
110        if ( $wgNoticeUseLanguageConversion ) {
111            $lang = MediaWikiServices::getInstance()->getLanguageFactory()
112                ->getParentLanguage( $lang->getCode() ) ?? $lang;
113        }
114
115        $text = self::sanitize(
116            $context->msg( $this->getDbKey() )->inLanguage( $lang )->text() );
117
118        return $text;
119    }
120
121    /**
122     * Add or update message contents
123     * @param string $translation
124     * @param string|null $lang
125     * @param User $user
126     * @param string|null $summary
127     */
128    public function update( $translation, $lang, $user, $summary = null ) {
129        global $wgNoticeUseTranslateExtension, $wgLanguageCode;
130
131        if ( $summary === null ) {
132            // default edit summary
133            // TODO make this consistent throughout CN
134            $summary = '/* CN admin */';
135        }
136
137        $savePage = static function ( $title, $text ) use ( $summary, $user ) {
138            $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
139
140            $content = ContentHandler::makeContent( $text, $title );
141            $tags = [ 'centralnotice' ];
142            $wikiPage->doUserEditContent(
143                $content,
144                $user,
145                $summary,
146                EDIT_FORCE_BOT,
147                false, // $originalRevId
148                $tags
149            );
150
151            return $wikiPage;
152        };
153
154        $page = $savePage( $this->getTitle( $lang ), $translation );
155        Banner::protectBannerContent( $page, $user );
156
157        // If we're using translate : group review; create and protect the english page
158        if ( $wgNoticeUseTranslateExtension
159            && ( $lang === $wgLanguageCode )
160            && BannerMessageGroup::isUsingGroupReview()
161        ) {
162            $page = $savePage( $this->getTitle( $lang, NS_CN_BANNER ), $translation );
163            Banner::protectBannerContent( $page, $user );
164        }
165    }
166
167    public static function sanitize( $text ) {
168        // First, remove any occurrences of the placeholders used to preserve span tags.
169        $text = str_replace( self::SPAN_TAG_PLACEHOLDER_START, '', $text );
170        $text = str_replace( self::SPAN_TAG_PLACEHOLDER_END, '', $text );
171
172        // Remove and save <span> tags so they don't get removed by sanitization; allow
173        // only class attributes.
174        $spanTags = [];
175        $text = preg_replace_callback(
176            '/(<\/?span\s*(?:class\s?=\s?([\'"])[a-zA-Z0-9_ -]+(\2))?\s*>)/',
177            static function ( $matches ) use ( &$spanTags ) {
178                $spanTags[] = $matches[ 1 ];
179                return BannerMessage::SPAN_TAG_PLACEHOLDER_START .
180                    ( count( $spanTags ) - 1 ) .
181                    BannerMessage::SPAN_TAG_PLACEHOLDER_END;
182            },
183            $text
184        );
185
186        $text = Sanitizer::stripAllTags( $text );
187        $text = Sanitizer::escapeHtmlAllowEntities( $text );
188
189        // Restore span tags
190        $text = preg_replace_callback(
191            '/(?:' . self::SPAN_TAG_PLACEHOLDER_START . '(\d+)' .
192                self::SPAN_TAG_PLACEHOLDER_END . ')/',
193
194            static function ( $matches ) use ( $spanTags ) {
195                $index = (int)$matches[ 1 ];
196                // This should never happen, but let's be safe.
197                if ( !isset( $spanTags[ $index ] ) ) {
198                    return '';
199                }
200                return $spanTags[ $index ];
201            },
202
203            $text
204        );
205
206        return $text;
207    }
208}