Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 249 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 249 |
|
0.00% |
0 / 19 |
5112 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
inEventSample | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
doEventLogging | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
272 | |||
doVisualEditorFeatureUseLogging | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
onEditPage__showEditForm_initial | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
182 | |||
onEditPage__showEditForm_fields | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
onGetPreferences | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getModuleData | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getModuleDataSummary | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getSignatureMessage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getMagicWords | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getEditingStatsId | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onEditPage__attemptSave | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
onEditPage__attemptSave_after | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
56 | |||
onEditPageGetPreviewContent | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onChangeTagsListActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onListDefinedTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onRecentChange_save | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Hooks for WikiEditor extension |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | */ |
8 | |
9 | namespace MediaWiki\Extension\WikiEditor; |
10 | |
11 | use ApiMessage; |
12 | use Article; |
13 | use Content; |
14 | use ExtensionRegistry; |
15 | use MediaWiki\Cache\CacheKeyHelper; |
16 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
17 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
18 | use MediaWiki\Config\Config; |
19 | use MediaWiki\Context\RequestContext; |
20 | use MediaWiki\EditPage\EditPage; |
21 | use MediaWiki\Extension\ConfirmEdit\Hooks as ConfirmEditHooks; |
22 | use MediaWiki\Extension\DiscussionTools\Hooks as DiscussionToolsHooks; |
23 | use MediaWiki\Extension\EventLogging\EventLogging; |
24 | use MediaWiki\Hook\EditPage__attemptSave_afterHook; |
25 | use MediaWiki\Hook\EditPage__attemptSaveHook; |
26 | use MediaWiki\Hook\EditPage__showEditForm_fieldsHook; |
27 | use MediaWiki\Hook\EditPage__showEditForm_initialHook; |
28 | use MediaWiki\Hook\EditPageGetPreviewContentHook; |
29 | use MediaWiki\Hook\RecentChange_saveHook; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Output\OutputPage; |
33 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
34 | use MediaWiki\Request\WebRequest; |
35 | use MediaWiki\ResourceLoader as RL; |
36 | use MediaWiki\Status\Status; |
37 | use MediaWiki\User\Options\UserOptionsLookup; |
38 | use MediaWiki\User\User; |
39 | use MediaWiki\User\UserEditTracker; |
40 | use MediaWiki\WikiMap\WikiMap; |
41 | use MessageLocalizer; |
42 | use MobileContext; |
43 | use MWCryptRand; |
44 | use RecentChange; |
45 | use WikimediaEvents\WikimediaEventsHooks; |
46 | |
47 | /** |
48 | * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
49 | */ |
50 | class Hooks implements |
51 | EditPage__showEditForm_initialHook, |
52 | EditPage__showEditForm_fieldsHook, |
53 | GetPreferencesHook, |
54 | EditPage__attemptSaveHook, |
55 | EditPage__attemptSave_afterHook, |
56 | EditPageGetPreviewContentHook, |
57 | ListDefinedTagsHook, |
58 | ChangeTagsListActiveHook, |
59 | RecentChange_saveHook |
60 | { |
61 | |
62 | /** @var string|bool ID used for grouping entries all of a session's entries together in EventLogging. */ |
63 | private static $statsId = false; |
64 | |
65 | /** @var string[] */ |
66 | private static $tags = [ 'wikieditor' ]; |
67 | |
68 | private Config $config; |
69 | private UserEditTracker $userEditTracker; |
70 | private UserOptionsLookup $userOptionsLookup; |
71 | private ?MobileContext $mobileContext; |
72 | |
73 | public function __construct( |
74 | Config $config, |
75 | UserEditTracker $userEditTracker, |
76 | UserOptionsLookup $userOptionsLookup, |
77 | ?MobileContext $mobileContext |
78 | ) { |
79 | $this->config = $config; |
80 | $this->userEditTracker = $userEditTracker; |
81 | $this->userOptionsLookup = $userOptionsLookup; |
82 | $this->mobileContext = $mobileContext; |
83 | } |
84 | |
85 | /** |
86 | * Should the current session be sampled for EventLogging? |
87 | * |
88 | * @param string $sessionId |
89 | * @return bool Whether to sample the session |
90 | */ |
91 | protected function inEventSample( string $sessionId ): bool { |
92 | // Sample 6.25% |
93 | $samplingRate = $this->config->has( 'WMESchemaEditAttemptStepSamplingRate' ) ? |
94 | $this->config->get( 'WMESchemaEditAttemptStepSamplingRate' ) : 0.0625; |
95 | |
96 | // (T314896) Convert whatever we've been given to a string of hex, as that's what EL needs |
97 | $hexValue = hash( 'md5', $sessionId, false ); |
98 | |
99 | $inSample = EventLogging::sessionInSample( |
100 | (int)( 1 / $samplingRate ), $hexValue |
101 | ); |
102 | return $inSample; |
103 | } |
104 | |
105 | /** |
106 | * Log stuff to the eventlogging_EditAttemptStep stream in a shape that conforms to the |
107 | * analytics/legacy/editattemptstep schema. |
108 | * |
109 | * If the EventLogging extension is not loaded, then this is a NOP. |
110 | * |
111 | * @see https://meta.wikimedia.org/wiki/Schema:EditAttemptStep |
112 | * |
113 | * @param string $action |
114 | * @param Article $article Which article (with full context, page, title, etc.) |
115 | * @param array $data Data to log for this action |
116 | * @return void |
117 | */ |
118 | public function doEventLogging( |
119 | string $action, |
120 | Article $article, |
121 | array $data = [] |
122 | ): void { |
123 | if ( defined( 'MW_PHPUNIT_TEST' ) ) { |
124 | return; |
125 | } |
126 | |
127 | $extensionRegistry = ExtensionRegistry::getInstance(); |
128 | if ( !$extensionRegistry->isLoaded( 'EventLogging' ) || !$extensionRegistry->isLoaded( 'WikimediaEvents' ) ) { |
129 | return; |
130 | } |
131 | if ( $extensionRegistry->isLoaded( 'MobileFrontend' ) && $this->mobileContext ) { |
132 | if ( $this->mobileContext->shouldDisplayMobileView() ) { |
133 | // on a MobileFrontend page the logging should be handled by it |
134 | return; |
135 | } |
136 | } |
137 | $inSample = $this->inEventSample( $data['editing_session_id'] ); |
138 | $shouldOversample = WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $article->getContext() ); |
139 | |
140 | $user = $article->getContext()->getUser(); |
141 | $page = $article->getPage(); |
142 | $title = $article->getTitle(); |
143 | $revisionRecord = $page->getRevisionRecord(); |
144 | $skin = $article->getContext()->getSkin(); |
145 | |
146 | $data = [ |
147 | 'action' => $action, |
148 | 'version' => 1, |
149 | 'is_oversample' => !$inSample, |
150 | 'editor_interface' => 'wikitext', |
151 | // @todo FIXME for other than 'desktop'. T249944 |
152 | 'platform' => 'desktop', |
153 | 'integration' => 'page', |
154 | 'page_id' => $page->getId(), |
155 | 'page_title' => $title->getPrefixedText(), |
156 | 'page_ns' => $title->getNamespace(), |
157 | 'revision_id' => $revisionRecord ? $revisionRecord->getId() : 0, |
158 | 'user_id' => $user->getId(), |
159 | 'user_is_temp' => $user->isTemp(), |
160 | 'user_editcount' => $this->userEditTracker->getUserEditCount( $user ) ?: 0, |
161 | 'mw_version' => MW_VERSION, |
162 | 'skin' => $skin ? $skin->getSkinName() : null, |
163 | 'is_bot' => $user->isRegistered() && $user->isBot(), |
164 | 'is_anon' => $user->isAnon(), |
165 | 'wiki' => WikiMap::getCurrentWikiId(), |
166 | ] + $data; |
167 | |
168 | $bucket = ExtensionRegistry::getInstance()->isLoaded( 'DiscussionTools' ) ? |
169 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
170 | DiscussionToolsHooks\HookUtils::determineUserABTestBucket( $user ) : false; |
171 | if ( $bucket ) { |
172 | $data['bucket'] = $bucket; |
173 | } |
174 | |
175 | if ( $user->isAnon() ) { |
176 | $data['user_class'] = 'IP'; |
177 | } |
178 | |
179 | if ( !$inSample && !$shouldOversample ) { |
180 | return; |
181 | } |
182 | |
183 | EventLogging::submit( |
184 | 'eventlogging_EditAttemptStep', |
185 | [ |
186 | '$schema' => '/analytics/legacy/editattemptstep/2.0.2', |
187 | 'event' => $data, |
188 | ] |
189 | ); |
190 | } |
191 | |
192 | /** |
193 | * Log stuff to EventLogging's Schema:VisualEditorFeatureUse - |
194 | * see https://meta.wikimedia.org/wiki/Schema:VisualEditorFeatureUse |
195 | * If you don't have EventLogging and WikimediaEvents installed, does nothing. |
196 | * |
197 | * @param string $feature |
198 | * @param string $action |
199 | * @param Article $article Which article (with full context, page, title, etc.) |
200 | * @param string $sessionId Session identifier |
201 | * @return bool Whether the event was logged or not. |
202 | */ |
203 | public function doVisualEditorFeatureUseLogging( |
204 | string $feature, |
205 | string $action, |
206 | Article $article, |
207 | string $sessionId |
208 | ): bool { |
209 | $extensionRegistry = ExtensionRegistry::getInstance(); |
210 | if ( !$extensionRegistry->isLoaded( 'EventLogging' ) || !$extensionRegistry->isLoaded( 'WikimediaEvents' ) ) { |
211 | return false; |
212 | } |
213 | $inSample = $this->inEventSample( $sessionId ); |
214 | $shouldOversample = WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $article->getContext() ); |
215 | if ( !$inSample && !$shouldOversample ) { |
216 | return false; |
217 | } |
218 | |
219 | $user = $article->getContext()->getUser(); |
220 | $editCount = $this->userEditTracker->getUserEditCount( $user ); |
221 | $data = [ |
222 | 'feature' => $feature, |
223 | 'action' => $action, |
224 | 'editingSessionId' => $sessionId, |
225 | // @todo FIXME for other than 'desktop'. T249944 |
226 | 'platform' => 'desktop', |
227 | 'integration' => 'page', |
228 | 'editor_interface' => 'wikitext', |
229 | 'user_id' => $user->getId(), |
230 | 'user_is_temp' => $user->isTemp(), |
231 | 'user_editcount' => $editCount ?: 0, |
232 | ]; |
233 | |
234 | $bucket = ExtensionRegistry::getInstance()->isLoaded( 'DiscussionTools' ) ? |
235 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
236 | DiscussionToolsHooks\HookUtils::determineUserABTestBucket( $user ) : false; |
237 | if ( $bucket ) { |
238 | $data['bucket'] = $bucket; |
239 | } |
240 | |
241 | // NOTE: The 'VisualEditorFeatureUse' event was migrated to the Event Platform and is no |
242 | // longer using the legacy EventLogging schema from metawiki. $revId is actually |
243 | // overridden by the EventLoggingSchemas extension attribute in |
244 | // WikimediaEvents/extension.json. |
245 | return EventLogging::logEvent( 'VisualEditorFeatureUse', -1, $data ); |
246 | } |
247 | |
248 | /** |
249 | * EditPage::showEditForm:initial hook |
250 | * |
251 | * Adds the modules to the edit form |
252 | * |
253 | * @param EditPage $editPage the current EditPage object. |
254 | * @param OutputPage $outputPage object. |
255 | */ |
256 | public function onEditPage__showEditForm_initial( $editPage, $outputPage ) { |
257 | if ( $editPage->contentModel !== CONTENT_MODEL_WIKITEXT ) { |
258 | return; |
259 | } |
260 | |
261 | $article = $editPage->getArticle(); |
262 | $request = $article->getContext()->getRequest(); |
263 | |
264 | // Add modules if enabled |
265 | $user = $article->getContext()->getUser(); |
266 | if ( $this->userOptionsLookup->getBoolOption( $user, 'usebetatoolbar' ) ) { |
267 | $outputPage->addModuleStyles( 'ext.wikiEditor.styles' ); |
268 | $outputPage->addModules( 'ext.wikiEditor' ); |
269 | if ( $this->config->get( 'WikiEditorRealtimePreview' ) ) { |
270 | $outputPage->addModules( 'ext.wikiEditor.realtimepreview' ); |
271 | } |
272 | } |
273 | |
274 | // Don't run this if the request was posted - we don't want to log 'init' when the |
275 | // user just pressed 'Show preview' or 'Show changes', or switched from VE keeping |
276 | // changes. |
277 | if ( ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) && !$request->wasPosted() ) { |
278 | $data = []; |
279 | $data['editing_session_id'] = self::getEditingStatsId( $request ); |
280 | $section = $request->getRawVal( 'section' ); |
281 | if ( $section !== null ) { |
282 | $data['init_type'] = 'section'; |
283 | } else { |
284 | $data['init_type'] = 'page'; |
285 | } |
286 | if ( $request->getHeader( 'Referer' ) ) { |
287 | if ( |
288 | $section === 'new' |
289 | || !$article->getPage()->exists() |
290 | ) { |
291 | $data['init_mechanism'] = 'new'; |
292 | } else { |
293 | $data['init_mechanism'] = 'click'; |
294 | } |
295 | } else { |
296 | if ( |
297 | $section === 'new' |
298 | || !$article->getPage()->exists() |
299 | ) { |
300 | $data['init_mechanism'] = 'url-new'; |
301 | } else { |
302 | $data['init_mechanism'] = 'url'; |
303 | } |
304 | } |
305 | if ( $request->getRawVal( 'wvprov' ) === 'sticky-header' ) { |
306 | $data['init_mechanism'] .= '-sticky-header'; |
307 | } |
308 | |
309 | $this->doEventLogging( 'init', $article, $data ); |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * EditPage::showEditForm:fields hook |
315 | * |
316 | * Adds the event fields to the edit form |
317 | * |
318 | * @param EditPage $editPage the current EditPage object. |
319 | * @param OutputPage $outputPage object. |
320 | */ |
321 | public function onEditPage__showEditForm_fields( $editPage, $outputPage ) { |
322 | $outputPage->addHTML( |
323 | Html::hidden( |
324 | 'wikieditorUsed', |
325 | '', |
326 | [ 'id' => 'wikieditorUsed' ] |
327 | ) |
328 | ); |
329 | |
330 | if ( $editPage->contentModel !== CONTENT_MODEL_WIKITEXT |
331 | || !ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) { |
332 | return; |
333 | } |
334 | |
335 | $req = $outputPage->getRequest(); |
336 | $editingStatsId = self::getEditingStatsId( $req ); |
337 | |
338 | $shouldOversample = ExtensionRegistry::getInstance()->isLoaded( 'WikimediaEvents' ) && |
339 | WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $outputPage->getContext() ); |
340 | |
341 | $outputPage->addHTML( |
342 | Html::hidden( |
343 | 'editingStatsId', |
344 | $editingStatsId, |
345 | [ 'id' => 'editingStatsId' ] |
346 | ) |
347 | ); |
348 | |
349 | if ( $shouldOversample ) { |
350 | $outputPage->addHTML( |
351 | Html::hidden( |
352 | 'editingStatsOversample', |
353 | 1, |
354 | [ 'id' => 'editingStatsOversample' ] |
355 | ) |
356 | ); |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * GetPreferences hook |
362 | * |
363 | * Adds WikiEditor-related items to the preferences |
364 | * |
365 | * @param User $user current user |
366 | * @param array &$defaultPreferences list of default user preference controls |
367 | */ |
368 | public function onGetPreferences( $user, &$defaultPreferences ) { |
369 | // Ideally this key would be 'wikieditor-toolbar' |
370 | $defaultPreferences['usebetatoolbar'] = [ |
371 | 'type' => 'toggle', |
372 | 'label-message' => 'wikieditor-toolbar-preference', |
373 | 'help-message' => 'wikieditor-toolbar-preference-help', |
374 | 'section' => 'editing/editor', |
375 | ]; |
376 | $defaultPreferences['wikieditor-realtimepreview'] = [ |
377 | 'type' => 'api', |
378 | ]; |
379 | } |
380 | |
381 | /** |
382 | * @param RL\Context $context |
383 | * @param Config $config |
384 | * @return array |
385 | */ |
386 | public static function getModuleData( RL\Context $context, Config $config ): array { |
387 | return [ |
388 | // expose magic words for use by the wikieditor toolbar |
389 | 'magicWords' => self::getMagicWords(), |
390 | 'signature' => self::getSignatureMessage( $context ), |
391 | 'realtimeDebounce' => $config->get( 'WikiEditorRealtimePreviewDebounce' ), |
392 | 'realtimeDisableDuration' => $config->get( 'WikiEditorRealtimeDisableDuration' ), |
393 | ]; |
394 | } |
395 | |
396 | /** |
397 | * @param RL\Context $context |
398 | * @param Config $config |
399 | * @return array |
400 | */ |
401 | public static function getModuleDataSummary( RL\Context $context, Config $config ): array { |
402 | return [ |
403 | 'magicWords' => self::getMagicWords(), |
404 | 'signature' => self::getSignatureMessage( $context, true ), |
405 | 'realtimeDebounce' => $config->get( 'WikiEditorRealtimePreviewDebounce' ), |
406 | 'realtimeDisableDuration' => $config->get( 'WikiEditorRealtimeDisableDuration' ), |
407 | ]; |
408 | } |
409 | |
410 | /** |
411 | * @param MessageLocalizer $ml |
412 | * @param bool $raw |
413 | * @return string |
414 | */ |
415 | private static function getSignatureMessage( MessageLocalizer $ml, bool $raw = false ): string { |
416 | $msg = $ml->msg( 'sig-text' )->params( '~~~~' )->inContentLanguage(); |
417 | return $raw ? $msg->plain() : $msg->text(); |
418 | } |
419 | |
420 | /** |
421 | * Expose useful magic words which are used by the wikieditor toolbar |
422 | * @return string[][] |
423 | */ |
424 | private static function getMagicWords(): array { |
425 | $requiredMagicWords = [ |
426 | 'redirect', |
427 | 'img_alt', |
428 | 'img_right', |
429 | 'img_left', |
430 | 'img_none', |
431 | 'img_center', |
432 | 'img_thumbnail', |
433 | 'img_framed', |
434 | 'img_frameless', |
435 | ]; |
436 | $magicWords = []; |
437 | $factory = MediaWikiServices::getInstance()->getMagicWordFactory(); |
438 | foreach ( $requiredMagicWords as $name ) { |
439 | $magicWords[$name] = $factory->get( $name )->getSynonyms(); |
440 | } |
441 | return $magicWords; |
442 | } |
443 | |
444 | /** |
445 | * Gets a 32 character alphanumeric random string to be used for stats. |
446 | * @param WebRequest $request |
447 | * @return string |
448 | */ |
449 | private static function getEditingStatsId( WebRequest $request ): string { |
450 | $fromRequest = $request->getRawVal( 'editingStatsId' ); |
451 | if ( $fromRequest !== null ) { |
452 | return $fromRequest; |
453 | } |
454 | if ( !self::$statsId ) { |
455 | self::$statsId = MWCryptRand::generateHex( 32 ); |
456 | } |
457 | return self::$statsId; |
458 | } |
459 | |
460 | /** |
461 | * This is attached to the MediaWiki 'EditPage::attemptSave' hook. |
462 | * |
463 | * @param EditPage $editPage |
464 | */ |
465 | public function onEditPage__attemptSave( $editPage ) { |
466 | $article = $editPage->getArticle(); |
467 | $request = $article->getContext()->getRequest(); |
468 | $statsId = $request->getRawVal( 'editingStatsId' ); |
469 | if ( $statsId !== null ) { |
470 | $this->doEventLogging( |
471 | 'saveAttempt', |
472 | $article, |
473 | [ 'editing_session_id' => $statsId ] |
474 | ); |
475 | } |
476 | } |
477 | |
478 | /** |
479 | * This is attached to the MediaWiki 'EditPage::attemptSave:after' hook. |
480 | * |
481 | * @param EditPage $editPage |
482 | * @param Status $status |
483 | * @param array $resultDetails |
484 | */ |
485 | public function onEditPage__attemptSave_after( $editPage, $status, $resultDetails ) { |
486 | $article = $editPage->getArticle(); |
487 | $request = $article->getContext()->getRequest(); |
488 | $statsId = $request->getRawVal( 'editingStatsId' ); |
489 | if ( $statsId !== null ) { |
490 | $data = []; |
491 | $data['editing_session_id'] = $statsId; |
492 | |
493 | if ( $status->isOK() ) { |
494 | $action = 'saveSuccess'; |
495 | |
496 | if ( $request->getRawVal( 'wikieditorUsed' ) === 'yes' ) { |
497 | $this->doVisualEditorFeatureUseLogging( |
498 | 'mwSave', 'source-has-js', $article, $statsId |
499 | ); |
500 | } |
501 | } else { |
502 | $action = 'saveFailure'; |
503 | |
504 | // Compare to ve.init.mw.ArticleTargetEvents.js in VisualEditor. |
505 | $typeMap = [ |
506 | 'badtoken' => 'userBadToken', |
507 | 'assertanonfailed' => 'userNewUser', |
508 | 'assertuserfailed' => 'userNewUser', |
509 | 'assertnameduserfailed' => 'userNewUser', |
510 | 'abusefilter-disallowed' => 'extensionAbuseFilter', |
511 | 'abusefilter-warning' => 'extensionAbuseFilter', |
512 | 'captcha' => 'extensionCaptcha', |
513 | 'spamblacklist' => 'extensionSpamBlacklist', |
514 | 'titleblacklist-forbidden' => 'extensionTitleBlacklist', |
515 | 'pagedeleted' => 'editPageDeleted', |
516 | 'editconflict' => 'editConflict' |
517 | ]; |
518 | |
519 | $errors = $status->getErrorsArray(); |
520 | // Replicate how the API generates error codes, in order to log data that is consistent with |
521 | // all other tools (which save changes via the API) |
522 | if ( isset( $errors[0] ) ) { |
523 | $code = ApiMessage::create( $errors[0] )->getApiCode(); |
524 | } else { |
525 | $code = 'unknown'; |
526 | } |
527 | |
528 | $wikiPage = $editPage->getArticle()->getPage(); |
529 | |
530 | if ( ExtensionRegistry::getInstance()->isLoaded( 'ConfirmEdit' ) ) { |
531 | $key = CacheKeyHelper::getKeyForPage( $wikiPage ); |
532 | /** @var SimpleCaptcha $captcha */ |
533 | $captcha = ConfirmEditHooks::getInstance(); |
534 | $activatedCaptchas = $captcha->getActivatedCaptchas(); |
535 | if ( isset( $activatedCaptchas[$key] ) ) { |
536 | // TODO: :( |
537 | $code = 'captcha'; |
538 | } |
539 | } |
540 | |
541 | $data['save_failure_message'] = $code; |
542 | $data['save_failure_type'] = $typeMap[ $code ] ?? 'responseUnknown'; |
543 | } |
544 | |
545 | $this->doEventLogging( $action, $article, $data ); |
546 | } |
547 | } |
548 | |
549 | /** |
550 | * Log a 'preview-nonlive' action when a page is previewed via the non-ajax full-page preview. |
551 | * |
552 | * @param EditPage $editPage |
553 | * @param Content &$content Content object to be previewed (may be replaced by hook function) |
554 | * @return bool|void True or no return value to continue or false to abort |
555 | */ |
556 | public function onEditPageGetPreviewContent( $editPage, &$content ) { |
557 | // This hook is only called for non-live previews, so we don't need to check the uselivepreview user option. |
558 | $editingStatsId = $editPage->getContext()->getRequest()->getRawVal( 'editingStatsId' ); |
559 | if ( $editingStatsId !== null ) { |
560 | $article = $editPage->getArticle(); |
561 | $this->doVisualEditorFeatureUseLogging( 'preview', 'preview-nonlive', $article, $editingStatsId ); |
562 | } |
563 | } |
564 | |
565 | /** |
566 | * @param string[] &$tags |
567 | * @return bool|void |
568 | */ |
569 | public function onChangeTagsListActive( &$tags ) { |
570 | $this->registerTags( $tags ); |
571 | } |
572 | |
573 | /** |
574 | * @param string[] &$tags |
575 | * @return bool|void |
576 | */ |
577 | public function onListDefinedTags( &$tags ) { |
578 | $this->registerTags( $tags ); |
579 | } |
580 | |
581 | /** |
582 | * @param string[] &$tags |
583 | */ |
584 | protected function registerTags( array &$tags ): void { |
585 | $tags = array_merge( $tags, static::$tags ); |
586 | } |
587 | |
588 | /** |
589 | * @param RecentChange $recentChange |
590 | * @return bool|void |
591 | */ |
592 | public function onRecentChange_save( $recentChange ) { |
593 | $request = RequestContext::getMain()->getRequest(); |
594 | if ( $request->getRawVal( 'wikieditorUsed' ) === 'yes' ) { |
595 | $recentChange->addTags( 'wikieditor' ); |
596 | } |
597 | } |
598 | } |