Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.14% covered (warning)
58.14%
50 / 86
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaskTypeHandler
58.14% covered (warning)
58.14%
50 / 86
16.67% covered (danger)
16.67%
2 / 12
131.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getId
n/a
0 / 0
n/a
0 / 0
0
 getSubmissionHandler
n/a
0 / 0
n/a
0 / 0
0
 validateTaskTypeConfiguration
84.38% covered (warning)
84.38%
27 / 32
0.00% covered (danger)
0.00%
0 / 1
13.64
 validateTemplate
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
5.02
 validateCategory
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
5.02
 validateTaskTypeObject
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createTaskType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchTerm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 createTaskFromSearchResult
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getChangeTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTaskTypeIdByChangeTagName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseExcludedTemplates
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseExcludedCategories
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\TaskType;
4
5use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationValidator;
6use GrowthExperiments\NewcomerTasks\SubmissionHandler;
7use GrowthExperiments\NewcomerTasks\Task\Task;
8use GrowthExperiments\NewcomerTasks\TaskSuggester\SearchStrategy\SearchQuery;
9use MediaWiki\Linker\LinkTarget;
10use MediaWiki\Title\MalformedTitleException;
11use MediaWiki\Title\TitleParser;
12use SearchResult;
13use StatusValue;
14
15/**
16 * A TaskTypeHandler is responsible for all the type-specific behavior of some TaskType
17 * (or group of TaskTypes) such as constructing the TaskType object from configuration or
18 * constructing the search query that corresponds to the TaskType.
19 *
20 * TaskTypeHandlers are identified by their can be obtained from the TaskTypeRegis
21 */
22abstract class TaskTypeHandler {
23
24    /**
25     * Change tag used to track edits made via suggested edit tasks. Subbtasks might add
26     * or replace with more specific tags.
27     */
28    public const NEWCOMER_TASK_TAG = 'newcomer task';
29
30    /** @var ConfigurationValidator */
31    protected $configurationValidator;
32    /** @var TitleParser */
33    private $titleParser;
34
35    /**
36     * @param ConfigurationValidator $configurationValidator
37     * @param TitleParser $titleParser
38     */
39    public function __construct( ConfigurationValidator $configurationValidator, TitleParser $titleParser ) {
40        $this->configurationValidator = $configurationValidator;
41        $this->titleParser = $titleParser;
42    }
43
44    /**
45     * Get the handler ID of this handler.
46     * This is mainly for internal use by TaskTypeHandlerRegistry.
47     * @return string
48     */
49    abstract public function getId(): string;
50
51    /**
52     * @return SubmissionHandler
53     */
54    abstract public function getSubmissionHandler(): SubmissionHandler;
55
56    /**
57     * Validate task configuration used by ConfigurationLoader.
58     * @param string $taskTypeId
59     * @param array $config
60     * @return StatusValue
61     * @see ::validateTaskTypeObject
62     */
63    public function validateTaskTypeConfiguration( string $taskTypeId, array $config ): StatusValue {
64        $status = StatusValue::newGood();
65        $status->merge( $this->configurationValidator->validateIdentifier( $taskTypeId ) );
66
67        $groupFieldStatus = $this->configurationValidator->validateRequiredField( 'group',
68            $config, $taskTypeId );
69        $status->merge( $groupFieldStatus );
70        if ( $groupFieldStatus->isOK() &&
71             !in_array( $config['group'], TaskType::DIFFICULTY_CLASSES, true )
72        ) {
73            $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidgroup',
74                $config['group'], $taskTypeId );
75        }
76
77        if ( $status->isOK() ) {
78            if ( !isset( $config['excludedTemplates'] ) ) {
79                $config['excludedTemplates'] = [];
80            }
81            $status->merge(
82                $this->configurationValidator->validateFieldIsArray( 'excludedTemplates', $config, $taskTypeId )
83            );
84            if ( $status->isOK() ) {
85                foreach ( $config['excludedTemplates'] as $template ) {
86                    $this->validateTemplate( $template, $taskTypeId, $status );
87                }
88            }
89        }
90
91        if ( $status->isOK() ) {
92            if ( !isset( $config['excludedCategories'] ) ) {
93                $config['excludedCategories'] = [];
94            }
95            $status->merge(
96                $this->configurationValidator->validateFieldIsArray( 'excludedCategories', $config, $taskTypeId )
97            );
98            if ( $status->isOK() ) {
99                foreach ( $config['excludedCategories'] as $category ) {
100                    $this->validateCategory( $category, $taskTypeId, $status );
101                }
102            }
103        }
104
105        // Link recommendation specific.
106        // FIXME: would be nice to define this and other type definitions in the task type.
107        if ( $status->isOK() && isset( $config['excludedSections'] ) ) {
108            $status->merge(
109                $this->configurationValidator->validateFieldIsArray( 'excludedSections', $config, $taskTypeId )
110            );
111        }
112
113        return $status;
114    }
115
116    /**
117     * Attempt to parse a template title, return a failed status value on MalformedTitleException.
118     *
119     * @param mixed $template
120     * @param string $taskTypeId
121     * @param StatusValue $status
122     * @return StatusValue
123     */
124    protected function validateTemplate( $template, string $taskTypeId, StatusValue $status ): StatusValue {
125        if ( !is_string( $template ) ) {
126            if ( !is_scalar( $template ) ) {
127                $template = '[' . gettype( $template ) . ']';
128            }
129            return $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidtemplatetitle',
130                $template, $taskTypeId );
131        }
132        try {
133            $this->titleParser->parseTitle( $template, NS_TEMPLATE );
134        } catch ( MalformedTitleException $e ) {
135            $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidtemplatetitle',
136                $template, $taskTypeId );
137        }
138        return $status;
139    }
140
141    /**
142     * Attempt to parse a category title, return a failed status value on MalformedTitleException.
143     *
144     * @param mixed $category
145     * @param string $taskTypeId
146     * @param StatusValue $status
147     * @return StatusValue
148     */
149    protected function validateCategory( $category, string $taskTypeId, StatusValue $status ): StatusValue {
150        if ( !is_string( $category ) ) {
151            if ( !is_scalar( $category ) ) {
152                $category = '[' . gettype( $category ) . ']';
153            }
154            return $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidcategorytitle',
155                $category, $taskTypeId );
156        }
157        try {
158            $this->titleParser->parseTitle( $category, NS_CATEGORY );
159        } catch ( MalformedTitleException $e ) {
160            $status->fatal( 'growthexperiments-homepage-suggestededits-config-invalidcategorytitle',
161                $category, $taskTypeId );
162        }
163        return $status;
164    }
165
166    /**
167     * Validate a task object. This is a companion to validateTaskTypeConfiguration() - some
168     * validation requires a TaskType object (typically checking whether messages exist) but
169     * first we need to make sure the configuration is valid enough to create the object,
170     * and the two cannot be done in the same method due to inheritance.
171     * @param TaskType $taskType
172     * @return StatusValue
173     */
174    public function validateTaskTypeObject( TaskType $taskType ): StatusValue {
175        return $this->configurationValidator->validateTaskMessages( $taskType );
176    }
177
178    /**
179     * @param string $taskTypeId
180     * @param array $config Task type configuration. Caller is assumed to have checked it
181     *   with validateTaskTypeConfiguration().
182     * @return TaskType
183     * @note Subclasses overriding this method must set the handled ID of the task type.
184     */
185    public function createTaskType( string $taskTypeId, array $config ): TaskType {
186        $extraData = [ 'learnMoreLink' => $config['learnmore'] ?? null ];
187        $taskType = new TaskType( $taskTypeId, $config['group'], $extraData );
188        $taskType->setHandlerId( $this->getId() );
189        return $taskType;
190    }
191
192    /**
193     * Get a CirrusSearch search term corresponding to this task.
194     *
195     * Task types extending this one must call this parent method to get exclusion search strings.
196     *
197     * @param TaskType $taskType
198     * @return string
199     */
200    public function getSearchTerm( TaskType $taskType ): string {
201        $searchTerm = '';
202        $excludedTemplates = $taskType->getExcludedTemplates();
203        if ( $excludedTemplates ) {
204            // extra space added to facilitate concatenation
205            $searchTerm .= '-hastemplate:' . Util::escapeSearchTitleList( $excludedTemplates ) . ' ';
206        }
207        $excludedCategories = $taskType->getExcludedCategories();
208        if ( $excludedCategories ) {
209            // extra space added to facilitate concatenation
210            $searchTerm .= '-incategory:' . Util::escapeSearchTitleList( $excludedCategories ) . ' ';
211        }
212        return $searchTerm;
213    }
214
215    /**
216     * @param SearchQuery $query
217     * @param SearchResult $match
218     * @return Task
219     */
220    public function createTaskFromSearchResult( SearchQuery $query, SearchResult $match ): Task {
221        $taskType = $query->getTaskType();
222        $topic = $query->getTopic();
223        $task = new Task( $taskType, $match->getTitle() );
224        if ( $topic ) {
225            $score = 0;
226            // CirrusSearch and our custom FauxSearchResultWithScore have this.
227            if ( method_exists( $match, 'getScore' ) ) {
228                // @phan-suppress-next-line PhanUndeclaredMethod
229                $score = $match->getScore();
230            }
231            $task->setTopics( [ $topic ], [ $topic->getId() => $score ] );
232        }
233
234        return $task;
235    }
236
237    /**
238     * Get the list of change tags to apply to edits originating from this task type.
239     * @param string|null $taskType
240     * @return string[]
241     */
242    public function getChangeTags( ?string $taskType = null ): array {
243        return [ self::NEWCOMER_TASK_TAG ];
244    }
245
246    /**
247     * Get the task type ID based on the change tag associated with it.
248     *
249     * @param string $changeTagName
250     * @return string|null
251     */
252    public function getTaskTypeIdByChangeTagName( string $changeTagName ): ?string {
253        return null;
254    }
255
256    /**
257     * @param array $config
258     * @return LinkTarget[]
259     * @throws MalformedTitleException
260     */
261    protected function parseExcludedTemplates( array $config ): array {
262        $excludedTemplates = [];
263        foreach ( $config['excludedTemplates'] ?? [] as $excludedTemplate ) {
264            $excludedTemplates[] = $this->titleParser->parseTitle( $excludedTemplate, NS_TEMPLATE );
265        }
266        return $excludedTemplates;
267    }
268
269    /**
270     * @param array $config
271     * @return LinkTarget[]
272     * @throws MalformedTitleException
273     */
274    protected function parseExcludedCategories( array $config ): array {
275        $excludedCategories = [];
276        foreach ( $config['excludedCategories'] ?? [] as $excludedCategory ) {
277            $excludedCategories[] = $this->titleParser->parseTitle( $excludedCategory, NS_CATEGORY );
278        }
279        return $excludedCategories;
280    }
281
282}