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
17
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
 isUsingMobileDomain
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAgentContextAttributes
100.00% covered (success)
100.00%
6 / 6
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%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getPerformerContextAttributes
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace MediaWiki\Extension\EventLogging\MetricsPlatform;
4
5use ExtensionRegistry;
6use IContextSource;
7use Language;
8use MediaWiki\Config\Config;
9use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketService;
10use MediaWiki\Languages\LanguageConverterFactory;
11use MediaWiki\MainConfigNames;
12use MediaWiki\Permissions\RestrictionStore;
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
117     *
118     * @return bool
119     */
120    protected function isUsingMobileDomain(): bool {
121        if ( $this->extensionRegistry->isLoaded( 'MobileFrontend' ) ) {
122            // @phan-suppress-next-line PhanUndeclaredClassMethod
123            return MobileContext::singleton()->usingMobileDomain();
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->isUsingMobileDomain() ? 'mobile_browser' : 'desktop_browser',
138        ];
139    }
140
141    /**
142     * @param IContextSource $contextSource
143     * @return array
144     */
145    private function getPageContextAttributes( IContextSource $contextSource ): array {
146        $output = $contextSource->getOutput();
147        $wikidataItemId = $output->getProperty( 'wikibase_item' );
148        $wikidataItemId = $wikidataItemId === null ? null : (string)$wikidataItemId;
149
150        $result = [
151
152            // The wikidata_id (int) context attribute is deprecated in favor of wikidata_qid
153            // (string). See T330459 and T332673 for detail.
154            'page_wikidata_qid' => $wikidataItemId,
155
156        ];
157
158        $title = $contextSource->getTitle();
159
160        // IContextSource::getTitle() can return null.
161        //
162        // TODO: Document under what circumstances this happens.
163        if ( !$title ) {
164            return $result;
165        }
166
167        $namespace = $title->getNamespace();
168
169        return $result + [
170                'page_id' => $title->getArticleID(),
171                'page_title' => $title->getDBkey(),
172                'page_namespace' => $namespace,
173                'page_namespace_name' => $this->namespaceInfo->getCanonicalName( $namespace ),
174                'page_revision_id' => $title->getLatestRevID(),
175                'page_content_language' => $title->getPageLanguage()->getCode(),
176                'page_is_redirect' => $title->isRedirect(),
177                'page_groups_allowed_to_move' => $this->restrictionStore->getRestrictions( $title, 'move' ),
178                'page_groups_allowed_to_edit' => $this->restrictionStore->getRestrictions( $title, 'edit' ),
179            ];
180    }
181
182    /**
183     * @param IContextSource $contextSource
184     * @return array
185     */
186    private function getMediaWikiContextAttributes( IContextSource $contextSource ): array {
187        $skin = $contextSource->getSkin();
188
189        $user = $contextSource->getUser();
190        $isDebugMode =
191            $this->userOptionsLookup->getIntOption( $user, 'eventlogging-display-web' ) === 1 ||
192            $this->userOptionsLookup->getIntOption( $user, 'eventlogging-display-console' ) === 1;
193
194        // TODO: Reevaluate whether the `mediawiki.is_production` contextual attribute is useful.
195        //  We should be able to determine this from the database name of the wiki during analysis.
196        $isProduction = strpos( MW_VERSION, 'wmf' ) !== false;
197
198        return [
199            'mediawiki_skin' => $skin->getSkinName(),
200            'mediawiki_version' => MW_VERSION,
201            'mediawiki_is_debug_mode' => $isDebugMode,
202            'mediawiki_is_production' => $isProduction,
203            'mediawiki_db_name' => $this->mainConfig->get( MainConfigNames::DBname ),
204            'mediawiki_site_content_language' => $this->contentLanguage->getCode(),
205        ];
206    }
207
208    /**
209     * @param IContextSource $contextSource
210     * @return array
211     */
212    private function getPerformerContextAttributes( IContextSource $contextSource ): array {
213        $user = $contextSource->getUser();
214        $userName = $user->isAnon() ? null : $user->getName();
215        $userLanguage = $contextSource->getLanguage();
216
217        $languageConverter = $this->languageConverterFactory->getLanguageConverter( $userLanguage );
218        $userLanguageVariant = $languageConverter->hasVariants() ? $languageConverter->getPreferredVariant() : null;
219
220        $userEditCount = $user->getEditCount();
221        $userEditCountBucket = $user->isAnon() ? null : $this->userBucketService->bucketEditCount( $userEditCount );
222
223        $registrationTimestamp = $user->getRegistration();
224
225        if ( $registrationTimestamp ) {
226            $registrationTimestamp = wfTimestamp( TS_ISO_8601, $registrationTimestamp );
227        }
228
229        $result = [
230            'performer_is_logged_in' => !$user->isAnon(),
231            'performer_id' => $user->getId(),
232            'performer_name' => $userName,
233            'performer_groups' => $this->userGroupManager->getUserEffectiveGroups( $user ),
234            'performer_is_bot' => $user->isBot(),
235            'performer_language' => $userLanguage->getCode(),
236            'performer_language_variant' => $userLanguageVariant,
237            'performer_edit_count' => $userEditCount,
238            'performer_edit_count_bucket' => $userEditCountBucket,
239            'performer_registration_dt' => $registrationTimestamp,
240        ];
241
242        // IContextSource::getTitle() can return null.
243        //
244        // TODO: Document under what circumstances this happens.
245        $title = $contextSource->getTitle();
246
247        if ( $title ) {
248            $result['performer_can_probably_edit_page'] = $user->probablyCan( 'edit', $title );
249        }
250
251        return $result;
252    }
253}