Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 222
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikimediaEventsHooks
0.00% covered (danger)
0.00%
0 / 222
0.00% covered (danger)
0.00%
0 / 17
6642
0.00% covered (danger)
0.00%
0 / 1
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onXAnalyticsSetHeader
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
210
 getModuleConfig
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 getModuleFile
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
272
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialSearchGoResult
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialSearchResults
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onRecentChange_save
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onRecentChangeSaveCrossWikiUpload
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 onRecentChangeSaveEditCampaign
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 onArticleViewHeader
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 shouldSchemaEditAttemptStepOversample
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onBeforeInitialize
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace WikimediaEvents;
6
7use Article;
8use ExtensionRegistry;
9use IContextSource;
10use ISearchResultSet;
11use MediaWiki\Actions\ActionEntryPoint;
12use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
13use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
14use MediaWiki\Config\Config;
15use MediaWiki\Deferred\DeferredUpdates;
16use MediaWiki\Hook\BeforeInitializeHook;
17use MediaWiki\Hook\BeforePageDisplayHook;
18use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
19use MediaWiki\Hook\RecentChange_saveHook;
20use MediaWiki\Hook\SpecialSearchGoResultHook;
21use MediaWiki\Hook\SpecialSearchResultsHook;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Output\OutputPage;
24use MediaWiki\Page\Hook\ArticleViewHeaderHook;
25use MediaWiki\Parser\ParserOutput;
26use MediaWiki\Request\WebRequest;
27use MediaWiki\ResourceLoader as RL;
28use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
29use MediaWiki\ResourceLoader\ResourceLoader;
30use MediaWiki\Revision\RevisionRecord;
31use MediaWiki\Storage\EditResult;
32use MediaWiki\Storage\Hook\PageSaveCompleteHook;
33use MediaWiki\Title\Title;
34use MediaWiki\User\User;
35use MediaWiki\User\UserIdentity;
36use MediaWiki\WikiMap\WikiMap;
37use MobileContext;
38use RecentChange;
39use RequestContext;
40use Skin;
41use WikimediaEvents\Hooks\HookRunner;
42use WikiPage;
43
44/**
45 * Hooks used for Wikimedia-related logging
46 *
47 * @author Ori Livneh <ori@wikimedia.org>
48 * @author Matthew Flaschen <mflaschen@wikimedia.org>
49 * @author Benny Situ <bsitu@wikimedia.org>
50 */
51class WikimediaEventsHooks implements
52    BeforeInitializeHook,
53    BeforePageDisplayHook,
54    PageSaveCompleteHook,
55    ArticleViewHeaderHook,
56    ListDefinedTagsHook,
57    ChangeTagsListActiveHook,
58    SpecialSearchGoResultHook,
59    SpecialSearchResultsHook,
60    RecentChange_saveHook,
61    ResourceLoaderRegisterModulesHook,
62    MakeGlobalVariablesScriptHook
63{
64
65    /**
66     * @param OutputPage $out
67     * @param Skin $skin
68     */
69    public function onBeforePageDisplay( $out, $skin ): void {
70        $out->addModules( 'ext.wikimediaEvents' );
71
72        if ( ExtensionRegistry::getInstance()->isLoaded( 'WikibaseRepository' ) ) {
73            // If we are in Wikibase Repo, load Wikibase module
74            $out->addModules( 'ext.wikimediaEvents.wikibase' );
75        }
76    }
77
78    /**
79     * On XAnalyticsSetHeader
80     *
81     * When adding new headers here please update the docs:
82     * https://wikitech.wikimedia.org/wiki/X-Analytics
83     *
84     * Insert a 'page_id' key with the page ID as value (if the request is for a page with a pageid)
85     * Insert a 'ns' key with the namespace ID as value (if the request is for a valid title)
86     * Insert a 'special' key with the resolved name of the special page (if the request is for a
87     * special page).  If the name does not resolve, special is set to 'unknown' (see T304362).
88     *
89     * Add a 'loggedIn' key with the value of 1 if the user is logged in
90     * @param OutputPage $out
91     * @param array &$headerItems
92     */
93    public static function onXAnalyticsSetHeader( OutputPage $out, array &$headerItems ): void {
94        $title = $out->getTitle();
95        if ( $title !== null && !defined( 'MW_API' ) ) {
96            $pageId = $title->getArticleID();
97            $headerItems['ns'] = $title->getNamespace();
98            if ( is_int( $pageId ) && $pageId > 0 ) {
99                $headerItems['page_id'] = $pageId;
100            }
101            if ( $title->isSpecialPage() ) {
102                [ $name, /* $subpage */ ] = MediaWikiServices::getInstance()->getSpecialPageFactory()
103                    ->resolveAlias( $title->getDBkey() );
104
105                $headerItems['special'] = $name ?? 'unknown';
106            }
107            $revId = $out->getRevisionId();
108            // The revision ID will be positive for page and diff views, as well
109            // as viewing old revisions.
110            if ( $revId > 0 ) {
111                $headerItems['rev_id'] = $revId;
112            }
113        }
114
115        if ( $out->getUser()->isRegistered() ) {
116            $headerItems['loggedIn'] = 1;
117        }
118    }
119
120    /**
121     * Log server-side event on successful page edit.
122     *
123     * Imported from EventLogging extension
124     *
125     * @param WikiPage $wikiPage
126     * @param UserIdentity $userIdentity
127     * @param string $summary
128     * @param int $flags
129     * @param RevisionRecord $revisionRecord
130     * @param EditResult $editResult
131     *
132     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
133     */
134    public function onPageSaveComplete(
135        $wikiPage,
136        $userIdentity,
137        $summary,
138        $flags,
139        $revisionRecord,
140        $editResult
141    ): void {
142        if ( PHP_SAPI === 'cli' ) {
143            return; // ignore maintenance scripts
144        }
145
146        // Discard null edits from these metrics as they do not produce a
147        // saved an edit to a page, and thus notably differ in code execution.
148        // Note that null edits can still be relatively slow, as they do perform
149        // reparses.
150        if ( $editResult->isNullEdit() ) {
151            return;
152        }
153
154        $title = $wikiPage->getTitle();
155
156        $request = RequestContext::getMain()->getRequest();
157        $services = MediaWikiServices::getInstance();
158        $nsInfo = $services->getNamespaceInfo();
159        $permMgr = $services->getPermissionManager();
160
161        $user = User::newFromIdentity( $userIdentity );
162        $content = $wikiPage->getContent();
163
164        if (
165            $user->isBot() ||
166            ( $request->getCheck( 'bot' ) && $permMgr->userHasRight( $user, 'bot' ) )
167        ) {
168            $accType = 'bot'; // registered bot or script acting on behalf of a user
169        } elseif ( $request->getCheck( 'maxlag' ) ) {
170            $accType = 'throttled'; // probably an unregistered bot
171        } else {
172            $accType = 'normal';
173        }
174
175        if ( in_array( $content->getModel(), [ 'wikibase-item', 'wikibase-property' ] ) ) {
176            $nsType = 'entity';
177        } elseif ( $nsInfo->isContent( $title->getNamespace() ) ) {
178            $nsType = 'content';
179        } elseif ( $nsInfo->isTalk( $title->getNamespace() ) ) {
180            $nsType = 'talk';
181        } else {
182            $nsType = 'meta';
183        }
184
185        if ( MW_ENTRY_POINT === 'index' ) {
186            // non-AJAX submission from user interface
187            // (for non-WMF this could also mean jobrunner, since jobs run post-send
188            // from index.php by default)
189            $entry = 'index';
190        } elseif ( MW_ENTRY_POINT === 'api' || MW_ENTRY_POINT === 'rest' ) {
191            $entry = 'api';
192        } else {
193            // jobrunner, maint/cli
194            $entry = 'other';
195        }
196
197        $size = $content->getSize();
198
199        DeferredUpdates::addCallableUpdate(
200            static function () use ( $size, $nsType, $accType, $entry ) {
201                $reqCtxTiming = RequestContext::getMain()->getTiming();
202
203                $measure = $reqCtxTiming->measure(
204                    'editResponseTime', 'requestStart', 'requestShutdown' );
205                if ( $measure === false ) {
206                    return;
207                }
208
209                $timeMs = $measure['duration'] * 1000;
210
211                $statsFactory = MediaWikiServices::getInstance()->getStatsFactory()
212                    ->withComponent( 'WikimediaEvents' );
213
214                $statsFactory->getTiming( 'editResponseTime_seconds' )
215                    ->setLabel( 'page', $nsType )
216                    ->setLabel( 'user', $accType )
217                    ->setLabel( 'entry', $entry )
218                    ->copyToStatsdAt( [
219                        "timing.editResponseTime",
220                        "timing.editResponseTime.page.$nsType",
221                        "timing.editResponseTime.user.$accType",
222                        "timing.editResponseTime.entry.$entry",
223                    ] )->observe( $timeMs );
224
225                $msPerKb = $timeMs / ( max( $size, 1 ) / 1e3 ); // T224686
226                $statsFactory->getTiming( 'editResponseTimePerKB_seconds' )
227                    ->setLabel( 'page', $nsType )
228                    ->setLabel( 'user', $accType )
229                    ->setLabel( 'entry', $entry )
230                    ->copyToStatsdAt( [
231                        "timing.editResponseTimePerKB.page.$nsType",
232                        "timing.editResponseTimePerKB.user.$accType",
233                        "timing.editResponseTimePerKB.entry.$entry",
234                    ] )->observe( $msPerKb );
235            }
236        );
237    }
238
239    /**
240     * Callback for ext.wikimediaEvents virtual config.json file.
241     *
242     * @param RL\Context $context
243     * @param Config $config
244     * @return array
245     */
246    public static function getModuleConfig( RL\Context $context, Config $config ) {
247        $vars = [];
248        $vars['clientErrorIntakeURL'] = $config->get( 'WMEClientErrorIntakeURL' );
249        $vars['statsdBaseUri'] = $config->get( 'WMEStatsdBaseUri' );
250        $vars['wikidataCompletionSearchClicks'] = $config->get( 'WMEWikidataCompletionSearchClicks' );
251        $vars['sessionTick'] = $config->get( 'WMESessionTick' );
252        $vars['readingDepthSamplingRate'] = $config->get( 'WMEReadingDepthSamplingRate' );
253        $vars['newPHPSamplingRate'] = $config->get( 'WMENewPHPSamplingRate' );
254        $vars['newPHPVersion'] = $config->get( 'WMENewPHPVersion' );
255        $skin = $context->getSkin();
256        if ( $skin === 'minerva' ) {
257            $vars['mobileWebUIActionsTracking'] = $config->get( 'WMEMobileWebUIActionsTracking' );
258        } elseif ( in_array( $skin, [ 'vector', 'vector-2022' ] ) ) {
259            $vars['desktopWebUIActionsTracking'] = $config->get( 'WMEDesktopWebUIActionsTracking' );
260            $vars['desktopWebUIActionsTrackingOversampleLoggedInUsers'] =
261                $config->get( 'WMEDesktopWebUIActionsTrackingOversampleLoggedInUsers' );
262            $vars['webUIScrollTrackingSamplingRate'] = $config->get( 'WMEWebUIScrollTrackingSamplingRate' );
263            $vars['webUIScrollTrackingSamplingRateAnons'] = $config->get( 'WMEWebUIScrollTrackingSamplingRateAnons' );
264            $vars['webUIScrollTrackingTimeToWaitBeforeScrollUp'] =
265                $config->get( 'WMEWebUIScrollTrackingTimeToWaitBeforeScrollUp' );
266        }
267
268        // editAttemptStep.js
269        $vars['WMESchemaEditAttemptStepSamplingRate'] =
270            $config->get( 'WMESchemaEditAttemptStepSamplingRate' );
271        $vars['WMESchemaVisualEditorFeatureUseSamplingRate'] =
272            $config->get( 'WMESchemaVisualEditorFeatureUseSamplingRate' );
273        $vars['DTSchemaEditAttemptStepSamplingRate'] =
274            $config->get( 'DTSchemaEditAttemptStepSamplingRate' );
275        $vars['DTSchemaEditAttemptStepOversample'] =
276            $config->get( 'DTSchemaEditAttemptStepOversample' );
277        $vars['MFSchemaEditAttemptStepOversample'] =
278            $config->get( 'MFSchemaEditAttemptStepOversample' );
279
280        return $vars;
281    }
282
283    /**
284     * Callback for dynamic source files, for conditional loading based on the current skin.
285     *
286     * @param RL\Context $context
287     * @param Config $config
288     * @param string $param callback param - corresponds to the file name to conditionally load
289     * @return RL\FilePath|string
290     */
291    public static function getModuleFile( RL\Context $context, Config $config, $param ) {
292        $skin = $context->getSkin();
293
294        switch ( $skin ) {
295            case 'vector':
296            case 'vector-2022':
297                switch ( $param ) {
298                    case 'clickTracking/desktop':
299                    case 'searchSatisfaction':
300                    case 'searchSli':
301                    case 'universalLanguageSelector':
302                    case 'webUIScroll':
303                        return new RL\FilePath( $param . '.js' );
304                    default:
305                        return '';
306                }
307            case 'minerva':
308                switch ( $param ) {
309                    case 'clickTracking/mobile':
310                        return new RL\FilePath( $param . '.js' );
311                    default:
312                        return '';
313                }
314            default:
315                switch ( $param ) {
316                    case 'searchSatisfaction':
317                    case 'searchSli':
318                        return new RL\FilePath( $param . '.js' );
319                    default:
320                        return '';
321                }
322        }
323    }
324
325    /**
326     * Register change tags.
327     *
328     * @param array &$tags
329     */
330    public function onListDefinedTags( &$tags ): void {
331        if ( WikiMap::getCurrentWikiId() === 'commonswiki' ) {
332            $tags[] = 'cross-wiki-upload';
333            // For A/B test
334            $tags[] = 'cross-wiki-upload-1';
335            $tags[] = 'cross-wiki-upload-2';
336            $tags[] = 'cross-wiki-upload-3';
337            $tags[] = 'cross-wiki-upload-4';
338        }
339    }
340
341    /**
342     * Mark active change tags.
343     *
344     * @param array &$tags
345     */
346    public function onChangeTagsListActive( &$tags ): void {
347        if ( WikiMap::getCurrentWikiId() === 'commonswiki' ) {
348            $tags[] = 'cross-wiki-upload';
349            // For A/B test
350            $tags[] = 'cross-wiki-upload-1';
351            $tags[] = 'cross-wiki-upload-2';
352            $tags[] = 'cross-wiki-upload-3';
353            $tags[] = 'cross-wiki-upload-4';
354        }
355    }
356
357    /**
358     * @param string $term
359     * @param Title $title
360     * @param string|null &$url
361     */
362    public function onSpecialSearchGoResult( $term, $title, &$url ): void {
363        $request = RequestContext::getMain()->getRequest();
364
365        $wprov = $request->getRawVal( 'wprov' );
366        if ( $wprov ) {
367            $url = $title->getFullURL( [ 'wprov' => $wprov ] );
368        }
369    }
370
371    /**
372     * The javascript that records search metrics needs to know if it is on a
373     * SERP or not. This ends up being non-trivial due to localization, so
374     * make it trivial by injecting a boolean value to check.
375     *
376     * @param string $term
377     * @param ?ISearchResultSet &$titleMatches
378     * @param ?ISearchResultSet &$textMatches
379     */
380    public function onSpecialSearchResults( $term, &$titleMatches, &$textMatches ): void {
381        $out = RequestContext::getMain()->getOutput();
382
383        $out->addJsConfigVars( [
384            'wgIsSearchResultPage' => true,
385        ] );
386    }
387
388    /**
389     * @param RecentChange $rc
390     */
391    public function onRecentChange_save( $rc ): void {
392        self::onRecentChangeSaveCrossWikiUpload( $rc );
393        self::onRecentChangeSaveEditCampaign( $rc );
394    }
395
396    /**
397     * Add a change tag 'cross-wiki-upload' to cross-wiki uploads to Commons, to track usage of the
398     * new feature. (Both to track adoption, and to let Commons editors review the uploads.) (T115328)
399     *
400     * @param RecentChange $rc
401     */
402    public static function onRecentChangeSaveCrossWikiUpload( RecentChange $rc ): void {
403        if ( !defined( 'MW_API' ) || WikiMap::getCurrentWikiId() !== 'commonswiki' ) {
404            return;
405        }
406
407        if (
408            $rc->getAttribute( 'rc_log_type' ) !== 'upload' ||
409            $rc->getAttribute( 'rc_log_action' ) !== 'upload'
410        ) {
411            return;
412        }
413        $request = RequestContext::getMain()->getRequest();
414        if ( !$request->response()->getHeader( 'Access-Control-Allow-Origin' ) ) {
415            return;
416        }
417
418        // A/B test
419        $bucket = $request->getRawVal( 'bucket' );
420        if ( !in_array( $bucket, [ '1', '2', '3', '4' ] ) ) {
421            $bucket = null;
422        }
423
424        $tags = [ 'cross-wiki-upload' ];
425        if ( $bucket ) {
426            $tags[] = "cross-wiki-upload-$bucket";
427        }
428        $rc->addTags( $tags );
429    }
430
431    /**
432     * Add a change tag 'campaign-...' to edits made via edit campaigns (identified by an URL
433     * parameter passed to the API via VisualEditor). (T209132)
434     *
435     * @param RecentChange $rc
436     */
437    public static function onRecentChangeSaveEditCampaign( RecentChange $rc ): void {
438        global $wgWMEEditCampaigns;
439
440        if ( !defined( 'MW_API' ) ) {
441            return;
442        }
443
444        $request = RequestContext::getMain()->getRequest();
445        $campaign = $request->getRawVal( 'campaign' );
446        if ( !in_array( $campaign, $wgWMEEditCampaigns ) ) {
447            return;
448        }
449
450        $tags = [ "campaign-$campaign" ];
451        $rc->addTags( $tags );
452    }
453
454    /**
455     * @param ResourceLoader $rl
456     */
457    public function onResourceLoaderRegisterModules( ResourceLoader $rl ): void {
458        if ( !ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) ) {
459            return;
460        }
461
462        $dir = dirname( __DIR__ ) . DIRECTORY_SEPARATOR;
463
464        $rl->register( "ext.wikimediaEvents.visualEditor", [
465            'localBasePath' => $dir . 'modules',
466            'remoteExtPath' => 'WikimediaEvents/modules',
467            "scripts" => "ext.wikimediaEvents.visualEditor/campaigns.js",
468            "dependencies" => "ext.visualEditor.targetLoader",
469        ] );
470    }
471
472    /**
473     * @param Article $article
474     * @param bool|ParserOutput|null &$outputDone
475     * @param bool &$pcache
476     */
477    public function onArticleViewHeader( $article, &$outputDone, &$pcache ) {
478        DeferredUpdates::addCallableUpdate( static function () {
479            $context = RequestContext::getMain();
480            $timing = $context->getTiming();
481            if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' )
482                && MobileContext::singleton()->shouldDisplayMobileView()
483            ) {
484                $platform = 'mobile';
485            } else {
486                $platform = 'desktop';
487            }
488
489            $measure = $timing->measure( 'viewResponseTime', 'requestStart', 'requestShutdown' );
490            if ( $measure !== false ) {
491                MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
492                    "timing.viewResponseTime.{$platform}", $measure['duration'] * 1000 );
493            }
494        } );
495    }
496
497    /**
498     * @param array &$vars
499     * @param OutputPage $out
500     */
501    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
502        $vars['wgWMESchemaEditAttemptStepOversample'] =
503            static::shouldSchemaEditAttemptStepOversample( $out->getContext() );
504
505        // Set page length for reading depth instrument T294777.
506        $length = $out->getTitle()->getLength();
507        $log = log10( $length );
508        $vars[ 'wgWMEPageLength' ] = round( $length, -intval( $log ) );
509    }
510
511    /**
512     * @param IContextSource $context
513     * @return bool
514     */
515    public static function shouldSchemaEditAttemptStepOversample( IContextSource $context ) {
516        // The editingStatsOversample request parameter can trigger oversampling
517        $shouldOversample = $context->getRequest()->getBool( 'editingStatsOversample' );
518        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
519            ->onWikimediaEventsShouldSchemaEditAttemptStepOversample( $context, $shouldOversample );
520        return $shouldOversample;
521    }
522
523    /**
524     * WMDE runs banner campaigns to encourage users to create an account and edit.
525     *
526     * The tracking already implemented in the Campaigns extension doesn't quite cover the WMDE
527     * use case. WMDE has a landing page that must be shown before the user progresses to
528     * registration. This could one day be factored out into its own extension, or made
529     * part of the Campaigns extension.
530     *
531     * Task for moving this to the Campaigns extension:
532     * https://phabricator.wikimedia.org/T174939
533     *
534     * Active WMDE campaigns tracked at:
535     * https://phabricator.wikimedia.org/project/subprojects/2821/
536     *
537     * @author addshore on behalf of WMDE
538     *
539     * @param Title $title
540     * @param mixed $unused
541     * @param OutputPage $output
542     * @param User $user
543     * @param WebRequest $request
544     * @param ActionEntryPoint $mediaWiki
545     */
546    public function onBeforeInitialize(
547        $title,
548        $unused,
549        $output,
550        $user,
551        $request,
552        $mediaWiki
553    ): void {
554        // Only run for dewiki
555        if ( WikiMap::getCurrentWikiId() !== 'dewiki' ) {
556            return;
557        }
558
559        // Setup the campaign prefix.
560        // Everything below this block is agnostic to which campaign is being run.
561        $campaignPrefix = 'WMDE_';
562        $cookieName = 'wmdecampaign-' . $campaignPrefix;
563
564        $hasCampaignCookie = $request->getCookie( $cookieName ) !== null;
565        $hasCampaignQuery = strpos( $request->getRawVal( 'campaign' ), $campaignPrefix ) === 0;
566
567        // Get the campaign name from either the cookie or query param
568        // Cookie has precedence
569        if ( $hasCampaignCookie ) {
570            $campaign = $request->getCookie( $cookieName );
571        } elseif ( $hasCampaignQuery ) {
572            $campaign = $request->getRawVal( 'campaign' );
573        } else {
574            // Request has nothing to do with our campaign
575            return;
576        }
577
578        // If an anon user clicks on the banner and doesn't yet have a session cookie then
579        // add a session cookie and log the click.
580        if ( !$hasCampaignCookie && $hasCampaignQuery && !$user->isRegistered() ) {
581            $request->response()->setCookie( $cookieName, $campaign, null );
582            wfDebugLog( 'WMDE', "$campaign - 1 - Banner click by anon user without cookie" );
583        }
584
585        // If an anon user with the cookie, views the create account page without a campaign
586        // query param, then inject it into the WebRequest object to influence the Campaigns
587        // extension.
588        if (
589            !$hasCampaignQuery &&
590            $hasCampaignCookie &&
591            !$user->isRegistered() &&
592            $title->isSpecial( 'CreateAccount' )
593        ) {
594            $request->setVal( 'campaign', $campaign );
595            wfDebugLog( 'WMDE', "$campaign - 2 - Inject campaign value on CreateAccount" );
596        }
597    }
598}