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    public function newContextAttributes( IContextSource $contextSource ): array {
102        $contextAttributes = [];
103        $contextAttributes += $this->getAgentContextAttributes();
104        $contextAttributes += $this->getPageContextAttributes( $contextSource );
105        $contextAttributes += $this->getMediaWikiContextAttributes( $contextSource );
106        $contextAttributes += $this->getPerformerContextAttributes( $contextSource );
107
108        return $contextAttributes;
109    }
110
111    /**
112     * Gets whether the user is accessing the mobile website
113     */
114    protected function shouldDisplayMobileView(): bool {
115        if ( $this->extensionRegistry->isLoaded( 'MobileFrontend' ) ) {
116            // @phan-suppress-next-line PhanUndeclaredClassMethod
117            return MobileContext::singleton()->shouldDisplayMobileView();
118        }
119
120        return false;
121    }
122
123    private function getAgentContextAttributes(): array {
124        return [
125            'agent_app_install_id' => null,
126            'agent_client_platform' => 'mediawiki_php',
127            'agent_client_platform_family' =>
128                $this->shouldDisplayMobileView() ? 'mobile_browser' : 'desktop_browser',
129            'agent_ua_string' => $_SERVER['HTTP_USER_AGENT'] ?? '',
130        ];
131    }
132
133    private function getPageContextAttributes( IContextSource $contextSource ): array {
134        $output = $contextSource->getOutput();
135        $wikidataItemId = $output->getProperty( 'wikibase_item' );
136        $wikidataItemId = $wikidataItemId === null ? null : (string)$wikidataItemId;
137
138        $result = [
139
140            // The wikidata_id (int) context attribute is deprecated in favor of wikidata_qid
141            // (string). See T330459 and T332673 for detail.
142            'page_wikidata_qid' => $wikidataItemId,
143
144        ];
145
146        $title = $contextSource->getTitle();
147
148        // IContextSource::getTitle() can return null.
149        //
150        // TODO: Document under what circumstances this happens.
151        if ( !$title ) {
152            return $result;
153        }
154
155        $namespaceId = $title->getNamespace();
156
157        return $result + [
158                'page_id' => $title->getArticleID(),
159                'page_title' => $title->getDBkey(),
160                'page_namespace_id' => $namespaceId,
161                'page_namespace_name' => $this->namespaceInfo->getCanonicalName( $namespaceId ),
162                'page_revision_id' => $title->getLatestRevID(),
163                'page_content_language' => $title->getPageLanguage()->getCode(),
164                'page_is_redirect' => $title->isRedirect(),
165                'page_groups_allowed_to_move' => $this->restrictionStore->getRestrictions( $title, 'move' ),
166                'page_groups_allowed_to_edit' => $this->restrictionStore->getRestrictions( $title, 'edit' ),
167            ];
168    }
169
170    private function getMediaWikiContextAttributes( IContextSource $contextSource ): array {
171        $skin = $contextSource->getSkin();
172
173        $user = $contextSource->getUser();
174        $isDebugMode = $this->userOptionsLookup->getIntOption( $user, 'eventlogging-display-console' ) === 1;
175
176        // TODO: Reevaluate whether the `mediawiki.is_production` contextual attribute is useful.
177        //  We should be able to determine this from the database name of the wiki during analysis.
178        $isProduction = strpos( MW_VERSION, 'wmf' ) !== false;
179
180        return [
181            'mediawiki_skin' => $skin->getSkinName(),
182            'mediawiki_version' => MW_VERSION,
183            'mediawiki_is_debug_mode' => $isDebugMode,
184            'mediawiki_is_production' => $isProduction,
185            'mediawiki_database' => $this->mainConfig->get( MainConfigNames::DBname ),
186            'mediawiki_site_content_language' => $this->contentLanguage->getCode(),
187        ];
188    }
189
190    private function getPerformerContextAttributes( IContextSource $contextSource ): array {
191        $user = $contextSource->getUser();
192        $userName = $user->isAnon() ? null : $user->getName();
193        $userLanguage = $contextSource->getLanguage();
194
195        $languageConverter = $this->languageConverterFactory->getLanguageConverter( $userLanguage );
196        $userLanguageVariant = $languageConverter->hasVariants() ? $languageConverter->getPreferredVariant() : null;
197
198        $userEditCount = $user->getEditCount();
199        $userEditCountBucket = $user->isAnon() ? null : $this->userBucketService->bucketEditCount( $userEditCount );
200
201        $result = [
202            'performer_is_logged_in' => !$user->isAnon(),
203            'performer_id' => $user->getId(),
204            'performer_name' => $userName,
205            'performer_groups' => $this->userGroupManager->getUserEffectiveGroups( $user ),
206            'performer_is_bot' => $user->isBot(),
207            'performer_is_temp' => $user->isTemp(),
208            'performer_language' => $userLanguage->getCode(),
209            'performer_language_variant' => $userLanguageVariant,
210            'performer_edit_count' => $userEditCount,
211            'performer_edit_count_bucket' => $userEditCountBucket
212        ];
213
214        // T408547 `$user-getRegistration()` returns `false` (which will fail when validating the event ) when
215        // the user is not registered
216        $registrationTimestamp = $user->getRegistration();
217        if ( $registrationTimestamp ) {
218            $registrationTimestamp = wfTimestamp( TS_ISO_8601, $registrationTimestamp );
219            $result['performer_registration_dt'] = $registrationTimestamp;
220        }
221
222        // IContextSource::getTitle() can return null.
223        //
224        // TODO: Document under what circumstances this happens.
225        $title = $contextSource->getTitle();
226
227        if ( $title ) {
228            $result['performer_can_probably_edit_page'] = $user->probablyCan( 'edit', $title );
229        }
230
231        return $result;
232    }
233}