Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
10.26% |
12 / 117 |
|
12.50% |
1 / 8 |
CRAP | |
0.00% |
0 / 1 |
MassMessage | |
10.26% |
12 / 117 |
|
12.50% |
1 / 8 |
514.60 | |
0.00% |
0 / 1 |
getMessengerUser | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
processPFData | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
90 | |||
parserError | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getQueuedCount | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
logToWiki | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
submit | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
20 | |||
getTimestampRegex | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
12 | |||
isSourceTranslationPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\MassMessage; |
4 | |
5 | use ExtensionRegistry; |
6 | use LogicException; |
7 | use ManualLogEntry; |
8 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage; |
9 | use MediaWiki\MassMessage\Job\MassMessageJob; |
10 | use MediaWiki\MassMessage\Job\MassMessageSubmitJob; |
11 | use MediaWiki\MassMessage\Lookup\DatabaseLookup; |
12 | use MediaWiki\MassMessage\Lookup\SpamlistLookup; |
13 | use MediaWiki\MassMessage\RequestProcessing\MassMessageRequest; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\User\CentralId\CentralIdLookup; |
17 | use MediaWiki\User\User; |
18 | use MediaWiki\WikiMap\WikiMap; |
19 | use ParserOptions; |
20 | |
21 | /** |
22 | * Some core functions needed by the extension. |
23 | * |
24 | * @file |
25 | * @author Kunal Mehta |
26 | * @license GPL-2.0-or-later |
27 | */ |
28 | |
29 | class MassMessage { |
30 | |
31 | /** |
32 | * Sets up the messenger account for our use if it hasn't been already. |
33 | * Based on code from AbuseFilter |
34 | * https://mediawiki.org/wiki/Extension:AbuseFilter |
35 | * |
36 | * @return User |
37 | */ |
38 | public static function getMessengerUser() { |
39 | $services = MediaWikiServices::getInstance(); |
40 | $userGroupManager = $services->getUserGroupManager(); |
41 | $userFactory = $services->getUserFactory(); |
42 | |
43 | $accountUsername = $services->getMainConfig()->get( 'MassMessageAccountUsername' ); |
44 | |
45 | // Only assign the bot flag if newSystemUser would either create |
46 | // or steal the account with the username specified. |
47 | $user = $userFactory->newFromName( $accountUsername ); |
48 | $shouldAssignBotFlag = !$user->isRegistered() || !$user->isSystemUser(); |
49 | |
50 | $user = User::newSystemUser( |
51 | $accountUsername, [ 'steal' => true ] |
52 | ); |
53 | // Make the user a bot so it doesn't look weird when the account was stolen |
54 | // or created. |
55 | if ( $shouldAssignBotFlag && !in_array( 'bot', $userGroupManager->getUserGroups( $user ) ) ) { |
56 | $userGroupManager->addUserToGroup( $user, 'bot' ); |
57 | } |
58 | |
59 | return $user; |
60 | } |
61 | |
62 | /** |
63 | * Verify that parser function data is valid and return processed data as an array |
64 | * @param string $page |
65 | * @param string $site |
66 | * @return array |
67 | */ |
68 | public static function processPFData( $page, $site ) { |
69 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
70 | |
71 | $titleObj = Title::newFromText( $page ); |
72 | if ( $titleObj === null ) { |
73 | return self::parserError( 'massmessage-parse-badpage', $page ); |
74 | } |
75 | |
76 | $currentWikiId = WikiMap::getCurrentWikiId(); |
77 | $data = [ 'title' => $page, 'site' => trim( $site ) ]; |
78 | if ( $data['site'] === '' ) { |
79 | $data['site'] = UrlHelper::getBaseUrl( $config->get( 'CanonicalServer' ) ); |
80 | $data['wiki'] = $currentWikiId; |
81 | } else { |
82 | $data['wiki'] = DatabaseLookup::getDBName( $data['site'] ); |
83 | if ( $data['wiki'] === null ) { |
84 | return self::parserError( 'massmessage-parse-badurl', $site ); |
85 | } |
86 | if ( !$config->get( 'AllowGlobalMessaging' ) && $data['wiki'] !== $currentWikiId ) { |
87 | return self::parserError( 'massmessage-global-disallowed' ); |
88 | } |
89 | } |
90 | if ( $data['wiki'] === $currentWikiId && $titleObj->isExternal() ) { |
91 | // interwiki links don't work |
92 | if ( $config->get( 'AllowGlobalMessaging' ) ) { |
93 | // tell them they need to use the |site= parameter |
94 | return self::parserError( 'massmessage-parse-badexternal', $page ); |
95 | } |
96 | // just tell them global messaging is disabled |
97 | return self::parserError( 'massmessage-global-disallowed' ); |
98 | } |
99 | return $data; |
100 | } |
101 | |
102 | /** |
103 | * Helper function for processPFData |
104 | * Inspired from the Cite extension |
105 | * @param string $key message key |
106 | * @param string|null $param parameter for the message |
107 | * @return array |
108 | */ |
109 | public static function parserError( $key, $param = null ) { |
110 | $msg = wfMessage( $key ); |
111 | if ( $param ) { |
112 | $msg->params( $param ); |
113 | } |
114 | return [ |
115 | '<strong class="error">' . |
116 | $msg->inContentLanguage()->plain() . |
117 | '</strong>', |
118 | 'noparse' => false, |
119 | 'error' => true, |
120 | ]; |
121 | } |
122 | |
123 | /** |
124 | * Get the number of Queued messages on this site |
125 | * Taken from runJobs.php --group |
126 | * @return int |
127 | */ |
128 | public static function getQueuedCount() { |
129 | $group = MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup(); |
130 | $queue = $group->get( 'MassMessageJob' ); |
131 | $pending = $queue->getSize(); |
132 | $claimed = $queue->getAcquiredCount(); |
133 | $abandoned = $queue->getAbandonedCount(); |
134 | $active = max( $claimed - $abandoned, 0 ); |
135 | |
136 | return $active + $pending; |
137 | } |
138 | |
139 | /** |
140 | * Log the spamming to Special:Log/massmessage |
141 | * |
142 | * @param Title $spamlist |
143 | * @param User $user |
144 | * @param string $subject |
145 | * @param string $pageMessage |
146 | */ |
147 | public static function logToWiki( |
148 | Title $spamlist, |
149 | User $user, |
150 | string $subject, |
151 | string $pageMessage |
152 | ): void { |
153 | $logEntry = new ManualLogEntry( 'massmessage', 'send' ); |
154 | $logEntry->setPerformer( $user ); |
155 | $logEntry->setTarget( $spamlist ); |
156 | $logEntry->setComment( $subject ); |
157 | $logEntry->setParameters( [ |
158 | '4::revid' => $spamlist->getLatestRevID(), |
159 | '5::pageMessage' => $pageMessage |
160 | ] ); |
161 | |
162 | $logid = $logEntry->insert(); |
163 | $logEntry->publish( $logid ); |
164 | } |
165 | |
166 | /** |
167 | * Send out the message! |
168 | * Note that this function does not perform validation on $data |
169 | * |
170 | * @param User $user who the message was from (for logging) |
171 | * @param MassMessageRequest $request |
172 | * @return int number of pages delivered to |
173 | */ |
174 | public static function submit( User $user, MassMessageRequest $request ): int { |
175 | // Get the array of pages to deliver to. |
176 | $pages = SpamlistLookup::getTargets( $request->getSpamList() ); |
177 | |
178 | // Log it. |
179 | self::logToWiki( $request->getSpamList(), $user, $request->getSubject(), $request->getPageMessage() ); |
180 | |
181 | $pageTitle = null; |
182 | $sourcePageLanguage = null; |
183 | $isSourceTranslationPage = false; |
184 | if ( $request->hasPageMessage() ) { |
185 | $pageTitle = Services::getInstance() |
186 | ->getLocalMessageContentFetcher() |
187 | ->getTitle( $request->getPageMessage() ) |
188 | ->getValue(); |
189 | $isSourceTranslationPage = self::isSourceTranslationPage( $pageTitle ); |
190 | if ( $isSourceTranslationPage ) { |
191 | $sourcePageLanguage = $pageTitle->getPageLanguage()->getCode(); |
192 | } |
193 | } |
194 | |
195 | $services = MediaWikiServices::getInstance(); |
196 | $originWiki = WikiMap::getCurrentWikiId(); |
197 | |
198 | $data = $request->getSerializedData(); |
199 | $data += [ |
200 | 'userId' => $services->getCentralIdLookup() |
201 | ->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ), |
202 | 'originWiki' => $originWiki, |
203 | 'isSourceTranslationPage' => $isSourceTranslationPage, |
204 | 'translationPageSourceLanguage' => $sourcePageLanguage, |
205 | 'pageMessageTitle' => $pageTitle ? $pageTitle->getPrefixedText() : null |
206 | ]; |
207 | |
208 | // Insert it into the job queue. |
209 | $params = [ |
210 | 'data' => $data, |
211 | 'pages' => $pages, |
212 | 'class' => MassMessageJob::class, |
213 | ]; |
214 | |
215 | $job = new MassMessageSubmitJob( $request->getSpamList(), $params ); |
216 | $services->getJobQueueGroupFactory()->makeJobQueueGroup( $originWiki )->push( $job ); |
217 | |
218 | return count( $pages ); |
219 | } |
220 | |
221 | /** |
222 | * Gets a regular expression that will match this wiki's |
223 | * timestamps as given by ~~~~. |
224 | * |
225 | * Modified from the Echo extension |
226 | * |
227 | * @return string regular expression fragment. |
228 | */ |
229 | public static function getTimestampRegex() { |
230 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
231 | |
232 | return $cache->getWithSetCallback( |
233 | $cache->makeKey( 'massmessage', 'timestamp' ), |
234 | $cache::TTL_WEEK, |
235 | static function () { |
236 | // Step 1: Get an exemplar timestamp |
237 | $title = Title::newMainPage(); |
238 | $user = User::newFromName( 'Test' ); |
239 | $options = new ParserOptions( $user ); |
240 | |
241 | $exemplarTimestamp = |
242 | MediaWikiServices::getInstance()->getParser() |
243 | ->preSaveTransform( '~~~~~', $title, $user, $options ); |
244 | |
245 | // Step 2: Generalise it |
246 | // Trim off the timezone to replace at the end |
247 | $output = $exemplarTimestamp; |
248 | $tzRegex = '/\s*\(\w+\)\s*$/'; |
249 | $output = preg_replace( $tzRegex, '', $output ); |
250 | $output = preg_quote( $output, '/' ); |
251 | $output = preg_replace( '/[^\d\W]+/u', '[^\d\W]+', $output ); |
252 | $output = preg_replace( '/\d+/u', '\d+', $output ); |
253 | |
254 | $tzMatches = []; |
255 | if ( preg_match( $tzRegex, $exemplarTimestamp, $tzMatches ) ) { |
256 | $output .= preg_quote( $tzMatches[0] ); |
257 | } |
258 | |
259 | if ( !preg_match( "/$output/u", $exemplarTimestamp ) ) { |
260 | throw new LogicException( "Timestamp regex does not match exemplar" ); |
261 | } |
262 | |
263 | return "/$output/"; |
264 | } |
265 | ); |
266 | } |
267 | |
268 | /** |
269 | * Checks if a title is a source translation page |
270 | * |
271 | * @param Title $title |
272 | * @return bool |
273 | */ |
274 | public static function isSourceTranslationPage( Title $title ): bool { |
275 | return ExtensionRegistry::getInstance()->isLoaded( 'Translate' ) && |
276 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
277 | TranslatablePage::isSourcePage( $title ); |
278 | } |
279 | } |