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