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