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