Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewcomerTasksChangeTagsManager
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 5
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 apply
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
30
 checkExistingTags
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 checkUserAccess
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 getTags
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\NewcomerTasks;
4
5use ChangeTags;
6use GrowthExperiments\HomepageModules\SuggestedEdits;
7use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
8use GrowthExperiments\NewcomerTasks\TaskType\TaskType;
9use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandler;
10use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandlerRegistry;
11use MediaWiki\Config\Config;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\Revision\RevisionLookup;
14use MediaWiki\User\Options\UserOptionsLookup;
15use MediaWiki\User\UserIdentity;
16use MediaWiki\User\UserIdentityUtils;
17use PrefixingStatsdDataFactoryProxy;
18use RequestContext;
19use StatusValue;
20use Wikimedia\Rdbms\IConnectionProvider;
21
22class NewcomerTasksChangeTagsManager {
23
24    /** @var ConfigurationLoader */
25    private $configurationLoader;
26    /** @var RevisionLookup */
27    private $revisionLookup;
28    /** @var TaskTypeHandlerRegistry */
29    private $taskTypeHandlerRegistry;
30    /** @var UserOptionsLookup */
31    private $userOptionsLookup;
32    /** @var PrefixingStatsdDataFactoryProxy */
33    private $perDbNameStatsdDataFactory;
34    /** @var IConnectionProvider */
35    private $connectionProvider;
36    /** @var UserIdentityUtils */
37    private $userIdentityUtils;
38    /** @var Config|null */
39    private $config;
40    /** @var UserIdentity|null */
41    private $user;
42
43    /**
44     * @param UserOptionsLookup $userOptionsLookup
45     * @param TaskTypeHandlerRegistry $taskTypeHandlerRegistry
46     * @param ConfigurationLoader $configurationLoader
47     * @param PrefixingStatsdDataFactoryProxy $perDbNameStatsdDataFactory
48     * @param RevisionLookup $revisionLookup
49     * @param IConnectionProvider $connectionProvider
50     * @param UserIdentityUtils $userIdentityUtils
51     * @param Config|null $config
52     * @param UserIdentity|null $user
53     * FIXME $config and $user should be mandatory and injected by a factory
54     */
55    public function __construct(
56        UserOptionsLookup $userOptionsLookup,
57        TaskTypeHandlerRegistry $taskTypeHandlerRegistry,
58        ConfigurationLoader $configurationLoader,
59        PrefixingStatsdDataFactoryProxy $perDbNameStatsdDataFactory,
60        RevisionLookup $revisionLookup,
61        IConnectionProvider $connectionProvider,
62        UserIdentityUtils $userIdentityUtils,
63        Config $config = null,
64        UserIdentity $user = null
65    ) {
66        $this->configurationLoader = $configurationLoader;
67        $this->revisionLookup = $revisionLookup;
68        $this->taskTypeHandlerRegistry = $taskTypeHandlerRegistry;
69        $this->userOptionsLookup = $userOptionsLookup;
70        $this->perDbNameStatsdDataFactory = $perDbNameStatsdDataFactory;
71        $this->connectionProvider = $connectionProvider;
72        $this->userIdentityUtils = $userIdentityUtils;
73        $this->config = $config;
74        $this->user = $user;
75    }
76
77    /**
78     * Apply change tags to a newcomer task.
79     *
80     * Note that this should only be used with non-VisualEditor based edits. VE edits are handled via
81     * the onVisualEditorApiVisualEditorEditPreSave hook, which also allows for displaying the change
82     * tags in the RecentChanges feed.
83     *
84     * Also note that using this method will set the tags for display in article history but it will
85     * not appear in RecentChanges (T24509).
86     *
87     * @param string $taskTypeId
88     * @param int $revisionId
89     * @param UserIdentity $userIdentity
90     * @return StatusValue
91     */
92    public function apply( string $taskTypeId, int $revisionId, UserIdentity $userIdentity ): StatusValue {
93        $result = $this->getTags( $taskTypeId, $userIdentity );
94
95        if ( !$result->isGood() ) {
96            return $result;
97        }
98        $tags = $result->getValue();
99
100        $revision = $this->revisionLookup->getRevisionById( $revisionId );
101        if ( !$revision ) {
102            return StatusValue::newFatal( $revisionId . ' is not a valid revision ID.' );
103        }
104        $revisionUserId = $revision->getUser()->getId();
105        $authorityUserId = $userIdentity->getId();
106        if ( $revisionUserId !== $authorityUserId ) {
107            return StatusValue::newFatal(
108                sprintf(
109                    'User ID %d on revision does not match logged-in user ID %d.', $revisionUserId, $authorityUserId
110                )
111            );
112        }
113
114        $result = $this->checkExistingTags( $revisionId );
115        if ( !$result->isGood() ) {
116            return $result;
117        }
118
119        $rc_id = null;
120        $log_id = null;
121        $result = ChangeTags::updateTags(
122            $tags,
123            null,
124            $rc_id,
125            $revisionId,
126            $log_id,
127            null,
128            null,
129            $userIdentity
130        );
131        LoggerFactory::getInstance( 'GrowthExperiments' )->debug(
132            'ChangeTags::updateTags result in NewcomerTaskCompleteHandler: ' . json_encode( $result )
133        );
134        // This is needed for non-VE edits.
135        // VE edits are incremented in the post-save VisualEditor hook.
136        $this->perDbNameStatsdDataFactory->increment(
137            'GrowthExperiments.NewcomerTask.' . $taskTypeId . '.Save'
138        );
139        return StatusValue::newGood( $result );
140    }
141
142    /**
143     * @param int $revId
144     * @return StatusValue
145     */
146    private function checkExistingTags( int $revId ): StatusValue {
147        $rc_id = null;
148        $log_id = null;
149        $existingTags = ChangeTags::getTags(
150            $this->connectionProvider->getReplicaDatabase(),
151            $rc_id,
152            $revId,
153            $log_id
154        );
155
156        // Guard against duplicate submissions, or re-tagging older revisions.
157        if ( in_array( TaskTypeHandler::NEWCOMER_TASK_TAG, $existingTags ) ) {
158            return StatusValue::newFatal( 'Revision already has newcomer task tag.' );
159        }
160        return StatusValue::newGood();
161    }
162
163    /**
164     * @param UserIdentity $userIdentity
165     * @return StatusValue
166     */
167    private function checkUserAccess( UserIdentity $userIdentity ): StatusValue {
168        if ( !$this->userIdentityUtils->isNamed( $userIdentity ) ) {
169            return StatusValue::newFatal( 'You must be logged-in' );
170        }
171        if ( !$this->config || !$this->user ) {
172            $ctx = RequestContext::getMain();
173            $this->config = $ctx->getConfig();
174            $this->user = $ctx->getUser();
175        }
176        if ( !SuggestedEdits::isEnabled( $this->config ) ||
177            !SuggestedEdits::isActivated( $this->user, $this->userOptionsLookup )
178        ) {
179            return StatusValue::newFatal( 'Suggested edits are not enabled or activated for your user.' );
180        }
181        return StatusValue::newGood();
182    }
183
184    /**
185     * @param string $taskTypeId
186     * @param UserIdentity $userIdentity
187     * @return StatusValue
188     */
189    public function getTags( string $taskTypeId, UserIdentity $userIdentity ): StatusValue {
190        $result = $this->checkUserAccess( $userIdentity );
191        if ( !$result->isGood() ) {
192            return $result;
193        }
194        $taskType = $this->configurationLoader->getTaskTypes()[$taskTypeId] ?? null;
195        if ( !$taskType instanceof TaskType ) {
196            return StatusValue::newFatal( 'Invalid task type ID: ' . $taskTypeId );
197        }
198        $taskTypeHandler = $this->taskTypeHandlerRegistry->getByTaskType( $taskType );
199        $tags = $taskTypeHandler->getChangeTags( $taskType->getId() );
200        return StatusValue::newGood( $tags );
201    }
202
203}