Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 7
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onRecentChange_save
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 onAbuseFilter_generateUserVars
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 onAbuseFilterBuilder
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMobileAppTagsFromRequest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\MobileApp;
4
5use ChangeTags;
6use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
7use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
8use MediaWiki\Context\RequestContext;
9use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
10use MediaWiki\Hook\RecentChange_saveHook;
11use MediaWiki\User\User;
12use RecentChange;
13use Wikimedia\Rdbms\IConnectionProvider;
14
15class Hooks implements
16    ListDefinedTagsHook,
17    ChangeTagsListActiveHook,
18    RecentChange_saveHook
19{
20    private IConnectionProvider $dbProvider;
21
22    private const USER_AGENT_TAGS = [
23        'mobile edit',
24        'mobile app edit',
25        'android app edit',
26        'ios app edit'
27    ];
28
29    private const APP_EDIT_TAGS = [
30        'app-suggestededit',
31        'app-undo',
32        'app-rollback',
33        'app-description-add',
34        'app-description-change',
35        'app-description-translate',
36        'app-section-source',
37        'app-full-source',
38        'app-select-source',
39        'app-talk-source',
40        'app-talk-reply',
41        'app-talk-topic',
42        'app-image-caption-add',
43        'app-image-caption-translate',
44        'app-image-tag-add',
45        'app-image-add-top',
46        'app-image-add-infobox',
47        'app-ai-assist'
48    ];
49
50    public function __construct(
51        IConnectionProvider $dbProvider
52    ) {
53        $this->dbProvider = $dbProvider;
54    }
55
56    /**
57     * ListDefinedTags hook handler
58     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ListDefinedTags
59     *
60     * @param array &$tags
61     */
62    public function onListDefinedTags( &$tags ) {
63        $tags = array_merge( $tags, static::USER_AGENT_TAGS, static::APP_EDIT_TAGS );
64    }
65
66    /**
67     * ChangeTagsListActive hook handler
68     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsListActive
69     *
70     * @param array &$tags
71     */
72    public function onChangeTagsListActive( &$tags ) {
73        $this->onListDefinedTags( $tags );
74    }
75
76    /**
77     * RecentChange_save hook handler that tags mobile changes
78     * @see https://www.mediawiki.org/wiki/Manual:Hooks/RecentChange_save
79     * @param RecentChange $rc
80     */
81    public function onRecentChange_save( $rc ) {
82        global $wgRequest;
83        $userAgent = $wgRequest->getHeader( "User-agent" );
84        $isWikipediaApp = strpos( $userAgent, "WikipediaApp/" ) === 0;
85        $isCommonsApp = strpos( $userAgent, "Commons/" ) === 0;
86        $logType = $rc->getAttribute( 'rc_log_type' );
87
88        // Apply tag for edits done with the Wikipedia app, and
89        // edits and uploads done with the Commons app
90        if (
91            ( $isWikipediaApp && $logType === null )
92            || ( $isCommonsApp && ( $logType === null || $logType === 'upload' ) )
93        ) {
94            // Although MobileFrontend applies the "mobile edit" tag to any edit
95            // that is made through the mobile domain, the Android app actually
96            // makes its API requests through the desktop domain, meaning that we
97            // must apply the "mobile edit" tag explicitly ourselves, in addition
98            // to the "mobile app edit" tag.
99            $tags = [ 'mobile edit', 'mobile app edit' ];
100
101            $isAndroid = strpos( $userAgent, "Android" ) > 0;
102            $isIOS = strpos( $userAgent, "iOS" ) > 0 || strpos( $userAgent, "iPadOS" ) > 0;
103
104            if ( $isAndroid ) {
105                $tags[] = 'android app edit';
106            } elseif ( $isIOS ) {
107                $tags[] = 'ios app edit';
108            }
109
110            $matags = static::getMobileAppTagsFromRequest();
111            if ( $matags ) {
112                $tags = array_merge( $tags, $matags );
113            }
114
115            $rc->addTags( $tags );
116        }
117    }
118
119    /**
120     * AbuseFilter-generateUserVars hook handler that adds the user_app variable.
121     *
122     * @see hooks.txt in AbuseFilter extension
123     * @param VariableHolder $vars object to add vars to
124     * @param User $user
125     * @param RecentChange|null $rc If the variables should be generated for an RC entry, this
126     *  is the entry. Null if it's for the current action being filtered.
127     * @return bool
128     */
129    public function onAbuseFilter_generateUserVars( $vars, $user, RecentChange $rc = null ) {
130        global $wgRequest;
131        if ( !$rc ) {
132            $userAgent = $wgRequest->getHeader( "User-agent" );
133            $isWikipediaApp = strpos( $userAgent, "WikipediaApp/" ) === 0;
134            $vars->setVar( 'user_app', $isWikipediaApp );
135        } else {
136            $dbr = $this->dbProvider->getReplicaDatabase();
137            $tags = ChangeTags::getTags( $dbr, $rc->getAttribute( 'rc_id' ) );
138            $vars->setVar( 'user_app', in_array( 'mobile app edit', $tags, true ) );
139        }
140        return true;
141    }
142
143    /**
144     * AbuseFilter-builder hook handler that adds user_app variable to list
145     *  of valid vars
146     *
147     * @param array &$builder Array in AbuseFilter::getBuilderValues to add to.
148     * @return bool
149     */
150    public static function onAbuseFilterBuilder( &$builder ) {
151        $builder['vars']['user_app'] = 'user-app';
152        return true;
153    }
154
155    /**
156     * Get MobileApp tags from the matags param in the request, and validate against known tags.
157     */
158    public static function getMobileAppTagsFromRequest(): array {
159        $request = RequestContext::getMain()->getRequest();
160        $tags = explode( ',', $request->getText( 'matags' ) );
161        return array_values( array_intersect( $tags, static::APP_EDIT_TAGS ) );
162    }
163}