Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.29% covered (success)
95.29%
81 / 85
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContextAttributesFactory
95.29% covered (success)
95.29%
81 / 85
71.43% covered (warning)
71.43%
5 / 7
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 newContextAttributes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 shouldDisplayMobileView
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAgentContextAttributes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getPageContextAttributes
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
3
 getMediaWikiContextAttributes
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 getPerformerContextAttributes
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace MediaWiki\Extension\EventLogging\MetricsPlatform;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Context\IContextSource;
7use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketService;
8use MediaWiki\Language\Language;
9use MediaWiki\Languages\LanguageConverterFactory;
10use MediaWiki\MainConfigNames;
11use MediaWiki\Permissions\RestrictionStore;
12use MediaWiki\Registration\ExtensionRegistry;
13use MediaWiki\Title\NamespaceInfo;
14use MediaWiki\User\Options\UserOptionsLookup;
15use MediaWiki\User\UserGroupManager;
16use MobileContext;
17
18/**
19 * @internal
20 */
21class ContextAttributesFactory {
22
23    /**
24     * @var Config
25     */
26    private $mainConfig;
27
28    /**
29     * @var ExtensionRegistry
30     */
31    private $extensionRegistry;
32
33    /**
34     * @var NamespaceInfo
35     */
36    private $namespaceInfo;
37
38    /**
39     * @var RestrictionStore
40     */
41    private $restrictionStore;
42
43    /**
44     * @var UserOptionsLookup
45     */
46    private $userOptionsLookup;
47
48    /**
49     * @var Language
50     */
51    private $contentLanguage;
52
53    /**
54     * @var UserGroupManager
55     */
56    private $userGroupManager;
57
58    /**
59     * @var LanguageConverterFactory
60     */
61    private $languageConverterFactory;
62
63    /**
64     * @var UserBucketService
65     */
66    private $userBucketService;
67
68    /**
69     * @param Config $mainConfig
70     * @param ExtensionRegistry $extensionRegistry
71     * @param NamespaceInfo $namespaceInfo
72     * @param RestrictionStore $restrictionStore
73     * @param UserOptionsLookup $userOptionsLookup
74     * @param Language $contentLanguage
75     * @param UserGroupManager $userGroupManager
76     * @param LanguageConverterFactory $languageConverterFactory
77     * @param UserBucketService $userBucketService
78     */
79    public function __construct(
80        Config $mainConfig,
81        ExtensionRegistry $extensionRegistry,
82        NamespaceInfo $namespaceInfo,
83        RestrictionStore $restrictionStore,
84        UserOptionsLookup $userOptionsLookup,
85        Language $contentLanguage,
86        UserGroupManager $userGroupManager,
87        LanguageConverterFactory $languageConverterFactory,
88        UserBucketService $userBucketService
89    ) {
90        $this->mainConfig = $mainConfig;
91        $this->extensionRegistry = $extensionRegistry;
92        $this->namespaceInfo = $namespaceInfo;
93        $this->restrictionStore = $restrictionStore;
94        $this->userOptionsLookup = $userOptionsLookup;
95        $this->contentLanguage = $contentLanguage;
96        $this->userGroupManager = $userGroupManager;
97        $this->languageConverterFactory = $languageConverterFactory;
98        $this->userBucketService = $userBucketService;
99    }
100
101    /**
102     * @param IContextSource $contextSource
103     * @return array
104     */
105    public function newContextAttributes( IContextSource $contextSource ): array {
106        $contextAttributes = [];
107        $contextAttributes += $this->getAgentContextAttributes();
108        $contextAttributes += $this->getPageContextAttributes( $contextSource );
109        $contextAttributes += $this->getMediaWikiContextAttributes( $contextSource );
110        $contextAttributes += $this->getPerformerContextAttributes( $contextSource );
111
112        return $contextAttributes;
113    }
114
115    /**
116     * Gets whether the user is accessing the mobile website
117     *
118     * @return bool
119     */
120    protected function shouldDisplayMobileView(): bool {
121        if ( $this->extensionRegistry->isLoaded( 'MobileFrontend' ) ) {
122            // @phan-suppress-next-line PhanUndeclaredClassMethod
123            return MobileContext::singleton()->shouldDisplayMobileView();
124        }
125
126        return false;
127    }
128
129    /**
130     * @return array
131     */
132    private function getAgentContextAttributes(): array {
133        return [
134            'agent_app_install_id' => null,
135            'agent_client_platform' => 'mediawiki_php',
136            'agent_client_platform_family' =>
137                $this->shouldDisplayMobileView() ? 'mobile_browser' : 'desktop_browser',
138            'agent_ua_string' => $_SERVER['HTTP_USER_AGENT'] ?? '',
139        ];
140    }
141
142    /**
143     * @param IContextSource $contextSource
144     * @return array
145     */
146    private function getPageContextAttributes( IContextSource $contextSource ): array {
147        $output = $contextSource->getOutput();
148        $wikidataItemId = $output->getProperty( 'wikibase_item' );
149        $wikidataItemId = $wikidataItemId === null ? null : (string)$wikidataItemId;
150
151        $result = [
152
153            // The wikidata_id (int) context attribute is deprecated in favor of wikidata_qid
154            // (string). See T330459 and T332673 for detail.
155            'page_wikidata_qid' => $wikidataItemId,
156
157        ];
158
159        $title = $contextSource->getTitle();
160
161        // IContextSource::getTitle() can return null.
162        //
163        // TODO: Document under what circumstances this happens.
164        if ( !$title ) {
165            return $result;
166        }
167
168        $namespaceId = $title->getNamespace();
169
170        return $result + [
171                'page_id' => $title->getArticleID(),
172                'page_title' => $title->getDBkey(),
173                'page_namespace_id' => $namespaceId,
174                'page_namespace_name' => $this->namespaceInfo->getCanonicalName( $namespaceId ),
175                'page_revision_id' => $title->getLatestRevID(),
176                'page_content_language' => $title->getPageLanguage()->getCode(),
177                'page_is_redirect' => $title->isRedirect(),
178                'page_groups_allowed_to_move' => $this->restrictionStore->getRestrictions( $title, 'move' ),
179                'page_groups_allowed_to_edit' => $this->restrictionStore->getRestrictions( $title, 'edit' ),
180            ];
181    }
182
183    /**
184     * @param IContextSource $contextSource
185     * @return array
186     */
187    private function getMediaWikiContextAttributes( IContextSource $contextSource ): array {
188        $skin = $contextSource->getSkin();
189
190        $user = $contextSource->getUser();
191        $isDebugMode = $this->userOptionsLookup->getIntOption( $user, 'eventlogging-display-console' ) === 1;
192
193        // TODO: Reevaluate whether the `mediawiki.is_production` contextual attribute is useful.
194        //  We should be able to determine this from the database name of the wiki during analysis.
195        $isProduction = strpos( MW_VERSION, 'wmf' ) !== false;
196
197        return [
198            'mediawiki_skin' => $skin->getSkinName(),
199            'mediawiki_version' => MW_VERSION,
200            'mediawiki_is_debug_mode' => $isDebugMode,
201            'mediawiki_is_production' => $isProduction,
202            'mediawiki_database' => $this->mainConfig->get( MainConfigNames::DBname ),
203            'mediawiki_site_content_language' => $this->contentLanguage->getCode(),
204        ];
205    }
206
207    /**
208     * @param IContextSource $contextSource
209     * @return array
210     */
211    private function getPerformerContextAttributes( IContextSource $contextSource ): array {
212        $user = $contextSource->getUser();
213        $userName = $user->isAnon() ? null : $user->getName();
214        $userLanguage = $contextSource->getLanguage();
215
216        $languageConverter = $this->languageConverterFactory->getLanguageConverter( $userLanguage );
217        $userLanguageVariant = $languageConverter->hasVariants() ? $languageConverter->getPreferredVariant() : null;
218
219        $userEditCount = $user->getEditCount();
220        $userEditCountBucket = $user->isAnon() ? null : $this->userBucketService->bucketEditCount( $userEditCount );
221
222        $result = [
223            'performer_is_logged_in' => !$user->isAnon(),
224            'performer_id' => $user->getId(),
225            'performer_name' => $userName,
226            'performer_groups' => $this->userGroupManager->getUserEffectiveGroups( $user ),
227            'performer_is_bot' => $user->isBot(),
228            'performer_is_temp' => $user->isTemp(),
229            'performer_language' => $userLanguage->getCode(),
230            'performer_language_variant' => $userLanguageVariant,
231            'performer_edit_count' => $userEditCount,
232            'performer_edit_count_bucket' => $userEditCountBucket
233        ];
234
235        // T408547 `$user-getRegistration()` returns `false` (which will fail when validating the event ) when
236        // the user is not registered
237        $registrationTimestamp = $user->getRegistration();
238        if ( $registrationTimestamp ) {
239            $registrationTimestamp = wfTimestamp( TS_ISO_8601, $registrationTimestamp );
240            $result['performer_registration_dt'] = $registrationTimestamp;
241        }
242
243        // IContextSource::getTitle() can return null.
244        //
245        // TODO: Document under what circumstances this happens.
246        $title = $contextSource->getTitle();
247
248        if ( $title ) {
249            $result['performer_can_probably_edit_page'] = $user->probablyCan( 'edit', $title );
250        }
251
252        return $result;
253    }
254}