Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
1.91% |
14 / 733 |
|
3.57% |
1 / 28 |
CRAP | |
0.00% |
0 / 1 |
SpecialEditGrowthConfig | |
1.91% |
14 / 733 |
|
3.57% |
1 / 28 |
10912.44 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
setConfigPage | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
isWikiConfigEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
displayRestrictionError | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
requiresWrite | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMessagePrefix | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preHtml | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
alterForm | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
72 | |||
getRawDescriptors | |
0.00% |
0 / 339 |
|
0.00% |
0 / 1 |
132 | |||
getValueGeConfig | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getNewcomerTasksConfig | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
getPrefixAndName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFormFields | |
0.00% |
0 / 106 |
|
0.00% |
0 / 1 |
306 | |||
normalizeSuggestedEditsIntroLinks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
normalizeHelpPanelLinks | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
preprocessSubmittedData | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
110 | |||
normalizeSuggestedEditsConfig | |
0.00% |
0 / 57 |
|
0.00% |
0 / 1 |
132 | |||
getDefaultDataForEnabledTaskTypes | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
normalizeTitleList | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
onSubmit | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
onSuccess | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getFeedbackHtml | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Specials; |
4 | |
5 | use GrowthExperiments\Config\GrowthExperimentsMultiConfig; |
6 | use GrowthExperiments\Config\Validation\GrowthConfigValidation; |
7 | use GrowthExperiments\Config\Validation\NewcomerTasksValidator; |
8 | use GrowthExperiments\Config\WikiPageConfigLoader; |
9 | use GrowthExperiments\Config\WikiPageConfigWriterFactory; |
10 | use GrowthExperiments\EventLogging\SpecialEditGrowthConfigLogger; |
11 | use GrowthExperiments\HomepageModules\Banner; |
12 | use GrowthExperiments\LevelingUp\LevelingUpManager; |
13 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskType; |
14 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler; |
15 | use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskType; |
16 | use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskTypeHandler; |
17 | use GrowthExperiments\NewcomerTasks\TaskType\SectionImageRecommendationTaskType; |
18 | use GrowthExperiments\NewcomerTasks\TaskType\SectionImageRecommendationTaskTypeHandler; |
19 | use MediaWiki\Html\Html; |
20 | use MediaWiki\HTMLForm\HTMLForm; |
21 | use MediaWiki\Message\Message; |
22 | use MediaWiki\Page\PageProps; |
23 | use MediaWiki\Revision\RevisionLookup; |
24 | use MediaWiki\SpecialPage\FormSpecialPage; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\Title\TitleFactory; |
28 | use MediaWiki\User\User; |
29 | use MediaWiki\Utils\MWTimestamp; |
30 | use OOUI\ButtonWidget; |
31 | use OOUI\IconWidget; |
32 | use PermissionsError; |
33 | use Wikimedia\Assert\Assert; |
34 | use Wikimedia\Rdbms\IDatabase; |
35 | use Wikimedia\Rdbms\ILoadBalancer; |
36 | use Wikimedia\Rdbms\ReadOnlyMode; |
37 | |
38 | class SpecialEditGrowthConfig extends FormSpecialPage { |
39 | /** @var string Right required to write */ |
40 | public const REQUIRED_RIGHT_TO_WRITE = 'editinterface'; |
41 | |
42 | /** @var string[] */ |
43 | private const SUGGESTED_EDITS_INTRO_LINKS = [ 'create', 'image' ]; |
44 | |
45 | /** @var string[] Keys that will be present in $configPages */ |
46 | private const CONFIG_PAGES_KEYS = [ 'geconfig', 'newcomertasks' ]; |
47 | |
48 | private TitleFactory $titleFactory; |
49 | private RevisionLookup $revisionLookup; |
50 | private PageProps $pageProps; |
51 | private ILoadBalancer $loadBalancer; |
52 | private ReadOnlyMode $readOnlyMode; |
53 | private WikiPageConfigLoader $configLoader; |
54 | private WikiPageConfigWriterFactory $configWriterFactory; |
55 | private GrowthExperimentsMultiConfig $growthWikiConfig; |
56 | private SpecialEditGrowthConfigLogger $eventLogger; |
57 | private ?string $errorMsgKey = null; |
58 | |
59 | /** |
60 | * @var Title[] |
61 | * |
62 | * All keys listed in CONFIG_PAGES_KEYS will be present, |
63 | * unless $errorMsgKey is not null (in which case the special page |
64 | * short-circuits anyway). |
65 | */ |
66 | private array $configPages = []; |
67 | private bool $userCanWrite; |
68 | private ?array $newcomerTasksConfig = null; |
69 | |
70 | /** |
71 | * @param TitleFactory $titleFactory |
72 | * @param RevisionLookup $revisionLookup |
73 | * @param PageProps $pageProps |
74 | * @param ILoadBalancer $loadBalancer |
75 | * @param ReadOnlyMode $readOnlyMode |
76 | * @param WikiPageConfigLoader $configLoader |
77 | * @param WikiPageConfigWriterFactory $configWriterFactory |
78 | * @param GrowthExperimentsMultiConfig $growthWikiConfig |
79 | */ |
80 | public function __construct( |
81 | TitleFactory $titleFactory, |
82 | RevisionLookup $revisionLookup, |
83 | PageProps $pageProps, |
84 | ILoadBalancer $loadBalancer, |
85 | ReadOnlyMode $readOnlyMode, |
86 | WikiPageConfigLoader $configLoader, |
87 | WikiPageConfigWriterFactory $configWriterFactory, |
88 | GrowthExperimentsMultiConfig $growthWikiConfig |
89 | ) { |
90 | parent::__construct( 'EditGrowthConfig' ); |
91 | |
92 | $this->titleFactory = $titleFactory; |
93 | $this->revisionLookup = $revisionLookup; |
94 | $this->pageProps = $pageProps; |
95 | $this->loadBalancer = $loadBalancer; |
96 | $this->readOnlyMode = $readOnlyMode; |
97 | $this->configLoader = $configLoader; |
98 | $this->configWriterFactory = $configWriterFactory; |
99 | $this->growthWikiConfig = $growthWikiConfig; |
100 | |
101 | $this->eventLogger = new SpecialEditGrowthConfigLogger(); |
102 | } |
103 | |
104 | /** @inheritDoc */ |
105 | protected function getGroupName() { |
106 | return 'growth-tools'; |
107 | } |
108 | |
109 | /** |
110 | * @param string|null $par |
111 | */ |
112 | public function execute( $par ) { |
113 | $this->getOutput()->enableOOUI(); |
114 | $this->addHelpLink( 'Growth/Community configuration' ); |
115 | |
116 | $config = $this->getConfig(); |
117 | $this->setConfigPage( |
118 | 'geconfig', |
119 | $config->get( 'GEWikiConfigPageTitle' ) |
120 | ); |
121 | $this->setConfigPage( |
122 | 'newcomertasks', |
123 | $config->get( 'GENewcomerTasksConfigTitle' ) |
124 | ); |
125 | |
126 | $this->userCanWrite = $this->getAuthority()->isAllowed( self::REQUIRED_RIGHT_TO_WRITE ); |
127 | |
128 | parent::execute( $par ); |
129 | |
130 | $this->eventLogger->logAction( SpecialEditGrowthConfigLogger::ACTION_VIEW, $this->getAuthority() ); |
131 | } |
132 | |
133 | /** |
134 | * Register a config page |
135 | * |
136 | * This validates the config page has proper content model |
137 | * and that it can be used to store config. |
138 | * |
139 | * @param string $key One of keys listed in CONFIG_PAGES_KEYS |
140 | * @param string $configPage |
141 | */ |
142 | private function setConfigPage( string $key, string $configPage ): void { |
143 | Assert::parameter( |
144 | in_array( $key, self::CONFIG_PAGES_KEYS ), |
145 | '$key', |
146 | 'must be one of keys listed in SpecialEditGrowthConfig::CONFIG_PAGES_KEYS' |
147 | ); |
148 | |
149 | $configTitle = $this->titleFactory->newFromText( $configPage ); |
150 | if ( |
151 | $configTitle === null || |
152 | !$configTitle->hasContentModel( CONTENT_MODEL_JSON ) |
153 | ) { |
154 | $this->errorMsgKey = 'growthexperiments-edit-config-error-invalid-title'; |
155 | return; |
156 | } |
157 | |
158 | $this->configPages[$key] = $configTitle; |
159 | } |
160 | |
161 | /** |
162 | * Determines if wiki config is enabled |
163 | * |
164 | * @return bool |
165 | */ |
166 | private function isWikiConfigEnabled(): bool { |
167 | return $this->growthWikiConfig->isWikiConfigEnabled(); |
168 | } |
169 | |
170 | /** |
171 | * @inheritDoc |
172 | */ |
173 | public function userCanExecute( User $user ) { |
174 | // Require both enabled wiki config and user-specific access level to |
175 | // be able to use the special page. |
176 | return $this->isWikiConfigEnabled() && parent::userCanExecute( $user ); |
177 | } |
178 | |
179 | /** |
180 | * @inheritDoc |
181 | */ |
182 | public function displayRestrictionError() { |
183 | if ( !$this->isWikiConfigEnabled() ) { |
184 | // Wiki config is disabled, display a meaningful restriction error |
185 | throw new PermissionsError( |
186 | null, |
187 | [ 'growthexperiments-edit-config-disabled' ] |
188 | ); |
189 | } |
190 | |
191 | // Otherwise, defer to the default logic |
192 | parent::displayRestrictionError(); |
193 | } |
194 | |
195 | /** |
196 | * @inheritDoc |
197 | */ |
198 | public function requiresWrite() { |
199 | return false; |
200 | } |
201 | |
202 | /** |
203 | * @inheritDoc |
204 | */ |
205 | public function doesWrites() { |
206 | return true; |
207 | } |
208 | |
209 | /** |
210 | * @inheritDoc |
211 | */ |
212 | protected function getMessagePrefix() { |
213 | return 'growthexperiments-edit-config'; |
214 | } |
215 | |
216 | /** |
217 | * @inheritDoc |
218 | */ |
219 | public function getDescription() { |
220 | return $this->msg( 'growthexperiments-edit-config-title' ); |
221 | } |
222 | |
223 | /** |
224 | * @inheritDoc |
225 | */ |
226 | protected function getDisplayFormat() { |
227 | return 'ooui'; |
228 | } |
229 | |
230 | /** |
231 | * @inheritDoc |
232 | */ |
233 | protected function preHtml() { |
234 | if ( $this->errorMsgKey !== null ) { |
235 | return $this->msg( $this->errorMsgKey )->escaped(); |
236 | } |
237 | return ''; |
238 | } |
239 | |
240 | /** |
241 | * Customize the form used |
242 | * |
243 | * This: |
244 | * * Hides the form if there is an error |
245 | * * Displays "last edited by" message |
246 | * * Displays an introduction message |
247 | * |
248 | * @param HTMLForm $form |
249 | */ |
250 | protected function alterForm( HTMLForm $form ) { |
251 | if ( $this->errorMsgKey !== null ) { |
252 | $form->suppressDefaultSubmit( true ); |
253 | return; |
254 | } |
255 | |
256 | if ( $this->userCanWrite ) { |
257 | $form->addPreHtml( $this->msg( |
258 | 'growthexperiments-edit-config-pretext', |
259 | Message::listParam( array_map( static function ( Title $title ) { |
260 | return '[[' . $title->getPrefixedText() . ']]'; |
261 | }, array_values( $this->configPages ) ) ) |
262 | )->parseAsBlock() ); |
263 | $form->addPreHtml( $this->msg( |
264 | 'growthexperiments-edit-config-pretext-banner', |
265 | $this->titleFactory->newFromText( |
266 | Banner::MESSAGE_KEY, |
267 | NS_MEDIAWIKI |
268 | )->getPrefixedText() |
269 | )->parseAsBlock() ); |
270 | } else { |
271 | $form->addPreHtml( $this->msg( 'growthexperiments-edit-config-pretext-unprivileged' ) ); |
272 | } |
273 | |
274 | // Add last updated data |
275 | foreach ( $this->configPages as $configType => $configTitle ) { |
276 | $revision = $this->revisionLookup->getRevisionByTitle( $configTitle ); |
277 | if ( $revision !== null ) { |
278 | $lastRevisionUser = $revision->getUser(); |
279 | $diffLink = $configTitle->getFullURL( [ 'oldid' => $revision->getId(), 'diff' => 'prev' ] ); |
280 | if ( $lastRevisionUser !== null ) { |
281 | $form->addPreHtml( $this->msg( |
282 | 'growthexperiments-edit-config-last-edit', |
283 | $lastRevisionUser->getName(), |
284 | MWTimestamp::getInstance( $revision->getTimestamp() ) |
285 | ->getRelativeTimestamp(), |
286 | $configTitle->getPrefixedText(), |
287 | $diffLink |
288 | )->parseAsBlock() ); |
289 | } else { |
290 | $form->addPreHtml( $this->msg( |
291 | 'growthexperiments-edit-config-last-edit-unknown-user', |
292 | MWTimestamp::getInstance( $revision->getTimestamp() ) |
293 | ->getRelativeTimestamp(), |
294 | $configTitle->getPrefixedText(), |
295 | $diffLink |
296 | )->parseAsBlock() ); |
297 | } |
298 | } |
299 | } |
300 | |
301 | $form->addPreHtml( $this->getFeedbackHtml() ); |
302 | |
303 | if ( !$this->userCanWrite ) { |
304 | $form->suppressDefaultSubmit( true ); |
305 | } elseif ( $this->readOnlyMode->isReadOnly() ) { |
306 | $form->suppressDefaultSubmit( true ); |
307 | $form->addPostHtml( $this->msg( 'readonlytext', $this->readOnlyMode->getReason() ) ); |
308 | } |
309 | } |
310 | |
311 | private function getRawDescriptors(): array { |
312 | // Whether the various pages configured as help links etc. must exist. |
313 | $pagesMustExist = !$this->getConfig()->get( 'GEDeveloperSetup' ); |
314 | |
315 | $descriptors = [ |
316 | // Growth experiments config (stored in MediaWiki:GrowthExperimentsConfig.json) |
317 | 'geconfig-GEHomepageSuggestedEditsIntroLinks-create' => [ |
318 | 'type' => 'title', |
319 | 'exists' => $pagesMustExist, |
320 | 'interwiki' => true, |
321 | 'label-message' => 'growthexperiments-edit-config-homepage-intro-links-create', |
322 | 'required' => true, |
323 | 'section' => 'homepage', |
324 | ], |
325 | 'geconfig-GEHomepageSuggestedEditsIntroLinks-image' => [ |
326 | 'type' => 'title', |
327 | 'exists' => $pagesMustExist, |
328 | 'interwiki' => true, |
329 | 'label-message' => 'growthexperiments-edit-config-homepage-intro-links-image', |
330 | 'required' => true, |
331 | 'section' => 'homepage', |
332 | ], |
333 | 'geconfig-mentorship-description' => [ |
334 | 'type' => 'info', |
335 | 'label-message' => 'growthexperiments-edit-config-mentorship-description-structured', |
336 | 'section' => 'mentorship', |
337 | ], |
338 | 'geconfig-GEMentorshipEnabled' => [ |
339 | 'type' => 'radio', |
340 | 'label-message' => 'growthexperiments-edit-config-mentorship-enabled', |
341 | 'options-messages' => [ |
342 | 'growthexperiments-edit-config-mentorship-enabled-true' => 'true', |
343 | 'growthexperiments-edit-config-mentorship-enabled-false' => 'false', |
344 | ], |
345 | 'section' => 'mentorship', |
346 | ], |
347 | 'geconfig-GEMentorshipAutomaticEligibility' => [ |
348 | 'type' => 'radio', |
349 | 'label-message' => 'growthexperiments-edit-config-mentorship-automatic-eligibility', |
350 | 'options-messages' => [ |
351 | 'growthexperiments-edit-config-mentorship-automatic-eligibility-true' => 'true', |
352 | 'growthexperiments-edit-config-mentorship-automatic-eligibility-false' => 'false', |
353 | ], |
354 | 'section' => 'mentorship', |
355 | ], |
356 | 'geconfig-GEMentorshipMinimumAge' => [ |
357 | 'type' => 'int', |
358 | 'label-message' => 'growthexperiments-edit-config-mentorship-minimum-age', |
359 | 'section' => 'mentorship', |
360 | ], |
361 | 'geconfig-GEMentorshipMinimumEditcount' => [ |
362 | 'type' => 'int', |
363 | 'label-message' => 'growthexperiments-edit-config-mentorship-minimum-editcount', |
364 | 'section' => 'mentorship', |
365 | ], |
366 | ]; |
367 | |
368 | if ( $this->getConfig()->get( 'GEPersonalizedPraiseBackendEnabled' ) ) { |
369 | $descriptors = array_merge( $descriptors, [ |
370 | 'geconfig-personalized-praise-description' => [ |
371 | 'type' => 'info', |
372 | 'label-message' => 'growthexperiments-edit-config-personalized-praise-description', |
373 | 'section' => 'personalized-praise', |
374 | ], |
375 | 'geconfig-GEPersonalizedPraiseDefaultNotificationsFrequency' => [ |
376 | 'type' => 'int', |
377 | 'label-message' => 'growthexperiments-edit-config-personalized-praise-notification-frequency', |
378 | 'section' => 'personalized-praise', |
379 | 'help-message' => 'growthexperiments-edit-config-personalized-praise-mentors-can-change', |
380 | ], |
381 | 'geconfig-GEPersonalizedPraiseMinEdits' => [ |
382 | 'type' => 'int', |
383 | 'label-message' => 'growthexperiments-edit-config-personalized-praise-min-edits', |
384 | 'section' => 'personalized-praise', |
385 | 'help-message' => 'growthexperiments-edit-config-personalized-praise-mentors-can-change', |
386 | ], |
387 | 'geconfig-GEPersonalizedPraiseDays' => [ |
388 | 'type' => 'int', |
389 | 'label-message' => 'growthexperiments-edit-config-personalized-praise-days', |
390 | 'section' => 'personalized-praise', |
391 | 'help-message' => 'growthexperiments-edit-config-personalized-praise-mentors-can-change', |
392 | ], |
393 | 'geconfig-GEPersonalizedPraiseMaxEdits' => [ |
394 | 'type' => 'int', |
395 | 'label-message' => 'growthexperiments-edit-config-personalized-praise-max-edits', |
396 | 'section' => 'personalized-praise', |
397 | ], |
398 | ] ); |
399 | } |
400 | |
401 | $descriptors = array_merge( $descriptors, [ |
402 | // Description for suggested edits config |
403 | 'newcomertasks-section-description' => [ |
404 | 'type' => 'info', |
405 | 'label-message' => 'growthexperiments-edit-config-newcomer-tasks-description', |
406 | 'section' => 'newcomertasks', |
407 | ], |
408 | ] ); |
409 | |
410 | $descriptors = array_merge( $descriptors, [ |
411 | 'geconfig-GEInfoboxTemplates' => [ |
412 | 'type' => 'titlesmultiselect', |
413 | 'exists' => $pagesMustExist, |
414 | 'placeholder' => $this->msg( 'nstab-template' )->text() . ':Infobox', |
415 | 'max' => GrowthConfigValidation::MAX_TEMPLATES_IN_COLLECTION, |
416 | 'label-message' => $this->msg( |
417 | 'growthexperiments-edit-config-newcomer-tasks-infobox-templates' |
418 | ), |
419 | 'help' => $this->msg( 'growthexperiments-edit-config-newcomer-tasks-infobox-templates-help' )->parse(), |
420 | 'required' => false, |
421 | 'section' => 'newcomertasks', |
422 | ] |
423 | ] ); |
424 | |
425 | // Add fields for suggested edits config (stored in MediaWiki:NewcomerTasks.json) |
426 | foreach ( $this->getDefaultDataForEnabledTaskTypes() as $taskType => $taskTypeData ) { |
427 | $isMachineSuggestionTaskType = in_array( |
428 | $taskType, |
429 | NewcomerTasksValidator::SUGGESTED_EDITS_MACHINE_SUGGESTIONS_TASK_TYPES |
430 | ); |
431 | $descriptors["newcomertasks-{$taskType}Info"] = [ |
432 | 'type' => 'info', |
433 | // TODO: It looks nicer to have each task type in its own section, but that's a bigger |
434 | // reorganization. |
435 | 'default' => '<h3>' . new IconWidget( [ 'icon' => $taskTypeData['icon'] ] ) . ' ' . |
436 | $this->msg( "growthexperiments-homepage-suggestededits-tasktype-name-$taskType" )->parse() . |
437 | '</h3>', |
438 | 'raw' => true, |
439 | 'section' => 'newcomertasks', |
440 | ]; |
441 | $descriptors["newcomertasks-{$taskType}Disabled"] = [ |
442 | 'type' => 'check', |
443 | 'label-message' => 'growthexperiments-edit-config-newcomer-tasks-disabled', |
444 | 'section' => 'newcomertasks', |
445 | ]; |
446 | $descriptors["newcomertasks-{$taskType}Templates"] = [ |
447 | 'type' => 'titlesmultiselect', |
448 | 'disabled' => $isMachineSuggestionTaskType, |
449 | 'exists' => $pagesMustExist, |
450 | 'namespace' => NS_TEMPLATE, |
451 | // TODO: This should be relative => true in an ideal world, see T285750 and |
452 | // T285748 for blockers |
453 | 'relative' => false, |
454 | 'label-message' => $isMachineSuggestionTaskType ? |
455 | "growthexperiments-edit-config-newcomer-tasks-machine-suggestions-no-templates" : |
456 | "growthexperiments-edit-config-newcomer-tasks-$taskType-templates", |
457 | 'required' => false, |
458 | 'section' => 'newcomertasks' |
459 | ]; |
460 | $descriptors["newcomertasks-{$taskType}ExcludedTemplates"] = [ |
461 | 'type' => 'titlesmultiselect', |
462 | 'exists' => $pagesMustExist, |
463 | 'namespace' => NS_TEMPLATE, |
464 | // TODO: This should be relative => true in an ideal world, see T285750 and |
465 | // T285748 for blockers |
466 | 'relative' => false, |
467 | 'label-message' => $this->msg( |
468 | "growthexperiments-edit-config-newcomer-tasks-excluded-templates" |
469 | ), |
470 | 'required' => false, |
471 | 'section' => 'newcomertasks' |
472 | ]; |
473 | $descriptors["newcomertasks-{$taskType}ExcludedCategories"] = [ |
474 | 'type' => 'titlesmultiselect', |
475 | 'exists' => $pagesMustExist, |
476 | 'namespace' => NS_CATEGORY, |
477 | // TODO: This should be relative => true in an ideal world, see T285750 and |
478 | // T285748 for blockers |
479 | 'relative' => false, |
480 | 'label-message' => $this->msg( |
481 | "growthexperiments-edit-config-newcomer-tasks-excluded-categories" |
482 | ), |
483 | 'required' => false, |
484 | 'section' => 'newcomertasks' |
485 | ]; |
486 | $descriptors["newcomertasks-{$taskType}Learnmore"] = [ |
487 | 'type' => 'title', |
488 | 'interwiki' => true, |
489 | 'exists' => $pagesMustExist, |
490 | 'label-message' => "growthexperiments-edit-config-newcomer-tasks-$taskType-learnmore", |
491 | 'required' => false, |
492 | 'section' => 'newcomertasks' |
493 | ]; |
494 | |
495 | if ( $taskType === LinkRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
496 | $descriptors["newcomertasks-link-recommendationMaximumLinksToShowPerTask"] = [ |
497 | 'type' => 'int', |
498 | 'default' => LinkRecommendationTaskType::DEFAULT_SETTINGS[ |
499 | LinkRecommendationTaskType::FIELD_MAX_LINKS_TO_SHOW_PER_TASK |
500 | ], |
501 | 'min' => LinkRecommendationTaskType::DEFAULT_SETTINGS[ |
502 | LinkRecommendationTaskType::FIELD_MIN_LINKS_PER_TASK |
503 | ], |
504 | 'max' => LinkRecommendationTaskType::DEFAULT_SETTINGS[ |
505 | LinkRecommendationTaskType::FIELD_MAX_LINKS_PER_TASK |
506 | ], |
507 | 'label-message' => |
508 | "growthexperiments-edit-config-newcomer-tasks-link-recommendation-maximum-links-to-show", |
509 | 'required' => false, |
510 | 'section' => 'newcomertasks' |
511 | ]; |
512 | |
513 | $descriptors['newcomertasks-link-recommendationMaxTasksPerDay'] = [ |
514 | 'type' => 'int', |
515 | 'default' => LinkRecommendationTaskType::DEFAULT_SETTINGS[ |
516 | LinkRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
517 | ], |
518 | 'label-message' => |
519 | 'growthexperiments-edit-config-newcomer-tasks-link-recommendation-maximum-tasks-per-day', |
520 | 'required' => false, |
521 | 'section' => 'newcomertasks' |
522 | ]; |
523 | |
524 | $descriptors["newcomertasks-link-recommendationExcludedSections"] = [ |
525 | 'type' => 'tagmultiselect', |
526 | 'allowArbitrary' => true, |
527 | // will be converted to string later |
528 | 'default' => LinkRecommendationTaskType::DEFAULT_SETTINGS[ |
529 | LinkRecommendationTaskType::FIELD_EXCLUDED_SECTIONS |
530 | ], |
531 | 'label-message' => |
532 | "growthexperiments-edit-config-newcomer-tasks-link-recommendation-excluded-sections", |
533 | 'help-message' => 'growthexperiments-edit-config-delayed', |
534 | 'required' => false, |
535 | 'section' => 'newcomertasks' |
536 | ]; |
537 | } elseif ( $taskType === ImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
538 | $descriptors['newcomertasks-image-recommendationMaxTasksPerDay'] = [ |
539 | 'type' => 'int', |
540 | 'default' => ImageRecommendationTaskType::DEFAULT_SETTINGS[ |
541 | ImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
542 | ], |
543 | 'label-message' => |
544 | 'growthexperiments-edit-config-newcomer-tasks-image-recommendation-maximum-tasks-per-day', |
545 | 'required' => false, |
546 | 'section' => 'newcomertasks' |
547 | ]; |
548 | } elseif ( $taskType === SectionImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
549 | $descriptors['newcomertasks-section-image-recommendationMaxTasksPerDay'] = [ |
550 | 'type' => 'int', |
551 | 'default' => SectionImageRecommendationTaskType::DEFAULT_SETTINGS[ |
552 | SectionImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
553 | ], |
554 | 'label-message' => |
555 | 'growthexperiments-edit-config-newcomer-tasks-section-image-' |
556 | . 'recommendation-maximum-tasks-per-day', |
557 | 'required' => false, |
558 | 'section' => 'newcomertasks' |
559 | ]; |
560 | } |
561 | } |
562 | |
563 | if ( LevelingUpManager::isEnabledForAnyone( $this->getConfig() ) ) { |
564 | $levelUpDescriptors = [ |
565 | 'geconfig-level-up-notifications-description' => [ |
566 | 'type' => 'info', |
567 | 'label-message' => 'growthexperiments-edit-config-level-up-notifications-description', |
568 | 'section' => 'level-up-notifications' |
569 | ], |
570 | 'geconfig-GELevelingUpGetStartedMaxTotalEdits' => [ |
571 | 'type' => 'int', |
572 | 'label-message' => 'growthexperiments-edit-config-try-suggested-edits-notification-title', |
573 | 'section' => 'level-up-notifications', |
574 | 'help-message' => 'growthexperiments-edit-config-try-suggested-edits-notification-description', |
575 | 'required' => true, |
576 | // NOTE: zero is used to disable the notification |
577 | 'min' => 0, |
578 | ], |
579 | 'geconfig-GELevelingUpKeepGoingNotificationThresholds-maximum' => [ |
580 | 'type' => 'int', |
581 | 'label-message' => 'growthexperiments-edit-config-keep-going-notification-title', |
582 | 'section' => 'level-up-notifications', |
583 | 'help-message' => 'growthexperiments-edit-config-keep-going-notification-description', |
584 | 'required' => true, |
585 | // NOTE: zero is used to disable the notification |
586 | 'min' => 0, |
587 | ] |
588 | ]; |
589 | |
590 | $descriptors = array_merge( $descriptors, $levelUpDescriptors ); |
591 | } |
592 | |
593 | $descriptors = array_merge( $descriptors, [ |
594 | 'geconfig-help-panel-description' => [ |
595 | 'type' => 'info', |
596 | 'label-message' => 'growthexperiments-edit-config-help-panel-description', |
597 | 'section' => 'help-panel', |
598 | ], |
599 | 'geconfig-GEHelpPanelExcludedNamespaces' => [ |
600 | 'type' => 'namespacesmultiselect', |
601 | 'exists' => true, |
602 | 'autocomplete' => false, |
603 | 'label-message' => 'growthexperiments-edit-config-help-panel-disabled-namespaces', |
604 | 'section' => 'help-panel', |
605 | ], |
606 | 'geconfig-GEHelpPanelReadingModeNamespaces' => [ |
607 | 'type' => 'namespacesmultiselect', |
608 | 'exists' => true, |
609 | 'autocomplete' => false, |
610 | 'label-message' => 'growthexperiments-edit-config-help-panel-reading-namespaces', |
611 | 'section' => 'help-panel', |
612 | ], |
613 | 'geconfig-GEHelpPanelSearchNamespaces' => [ |
614 | 'type' => 'namespacesmultiselect', |
615 | 'exists' => true, |
616 | 'autocomplete' => false, |
617 | 'label-message' => 'growthexperiments-edit-config-help-panel-searched-namespaces', |
618 | 'section' => 'help-panel', |
619 | ], |
620 | 'geconfig-GEHelpPanelAskMentor' => [ |
621 | 'type' => 'radio', |
622 | 'label-message' => 'growthexperiments-edit-config-help-panel-ask-mentor', |
623 | 'options-messages' => [ |
624 | 'growthexperiments-edit-config-help-panel-ask-mentor-true' => 'true', |
625 | 'growthexperiments-edit-config-help-panel-ask-mentor-false' => 'false', |
626 | ], |
627 | 'section' => 'help-panel', |
628 | ], |
629 | 'geconfig-GEHelpPanelHelpDeskTitle' => [ |
630 | 'type' => 'title', |
631 | 'exists' => $pagesMustExist, |
632 | 'label-message' => 'growthexperiments-edit-config-help-panel-helpdesk-title', |
633 | 'required' => false, |
634 | 'section' => 'help-panel', |
635 | ], |
636 | 'geconfig-GEHelpPanelHelpDeskPostOnTop' => [ |
637 | 'type' => 'radio', |
638 | 'label-message' => 'growthexperiments-edit-config-help-panel-post-on-top', |
639 | 'options-messages' => [ |
640 | 'growthexperiments-edit-config-help-panel-post-on-top-true' => 'true', |
641 | 'growthexperiments-edit-config-help-panel-post-on-top-false' => 'false', |
642 | ], |
643 | 'section' => 'help-panel', |
644 | ], |
645 | 'geconfig-GEHelpPanelLinks-description' => [ |
646 | 'type' => 'info', |
647 | 'label-message' => 'growthexperiments-edit-config-help-panel-links-description', |
648 | 'section' => 'help-panel-links', |
649 | ], |
650 | ] ); |
651 | |
652 | foreach ( [ 'mos', 'editing', 'images', 'references', 'articlewizard' ] as $position => $type ) { |
653 | // Messages used here (giving grep a chance to find usages): |
654 | // * growthexperiments-edit-config-help-panel-links-mos-title |
655 | // * growthexperiments-edit-config-help-panel-links-mos-label |
656 | // * growthexperiments-edit-config-help-panel-links-editing-title |
657 | // * growthexperiments-edit-config-help-panel-links-editing-label |
658 | // * growthexperiments-edit-config-help-panel-links-images-title |
659 | // * growthexperiments-edit-config-help-panel-links-images-label |
660 | // * growthexperiments-edit-config-help-panel-links-references-title |
661 | // * growthexperiments-edit-config-help-panel-links-references-label |
662 | // * growthexperiments-edit-config-help-panel-links-articlewizard-title |
663 | // * growthexperiments-edit-config-help-panel-links-articlewizard-label |
664 | $descriptors = array_merge( $descriptors, [ |
665 | "geconfig-GEHelpPanelLinks-$position-title" => [ |
666 | 'type' => 'title', |
667 | 'label-message' => "growthexperiments-edit-config-help-panel-links-$type-title", |
668 | 'section' => 'help-panel-links', |
669 | 'required' => false, |
670 | 'exists' => $pagesMustExist, |
671 | 'interwiki' => true, |
672 | ], |
673 | "geconfig-GEHelpPanelLinks-$position-label" => [ |
674 | 'type' => 'text', |
675 | 'label-message' => "growthexperiments-edit-config-help-panel-links-$type-label", |
676 | 'section' => 'help-panel-links', |
677 | ], |
678 | ] ); |
679 | } |
680 | |
681 | $descriptors = array_merge( $descriptors, [ |
682 | 'geconfig-GEHelpPanelViewMoreTitle' => [ |
683 | 'type' => 'title', |
684 | 'exists' => $pagesMustExist, |
685 | 'label-message' => 'growthexperiments-edit-config-help-panel-view-more', |
686 | 'required' => false, |
687 | 'interwiki' => true, |
688 | 'section' => 'help-panel-links', |
689 | ], |
690 | ] ); |
691 | |
692 | if ( !$this->userCanWrite ) { |
693 | foreach ( $descriptors as $key => $descriptor ) { |
694 | $descriptors[$key]['disabled'] = true; |
695 | } |
696 | } |
697 | |
698 | return $descriptors; |
699 | } |
700 | |
701 | /** |
702 | * Provide current value for a GrowthExperimentsMultiConfig variable |
703 | * |
704 | * @param string $name |
705 | * @return string|null |
706 | */ |
707 | private function getValueGeConfig( string $name ): ?string { |
708 | $default = $this->growthWikiConfig->getWithFlags( |
709 | $name, |
710 | GrowthExperimentsMultiConfig::READ_UNCACHED |
711 | ); |
712 | if ( is_array( $default ) ) { |
713 | $default = implode( "\n", $default ); |
714 | } |
715 | if ( is_bool( $default ) ) { |
716 | $default = $default ? 'true' : 'false'; |
717 | } |
718 | |
719 | return $default; |
720 | } |
721 | |
722 | /** |
723 | * Get newcomer tasks config. Avoid normal cache, use in-process cache only. |
724 | * |
725 | * @return array |
726 | */ |
727 | private function getNewcomerTasksConfig(): array { |
728 | if ( $this->newcomerTasksConfig !== null ) { |
729 | return $this->newcomerTasksConfig; |
730 | } |
731 | |
732 | $title = $this->titleFactory->newFromText( |
733 | $this->getConfig()->get( 'GENewcomerTasksConfigTitle' ) |
734 | ); |
735 | if ( $title === null || !$title->exists() ) { |
736 | return []; |
737 | } |
738 | |
739 | $res = $this->configLoader->load( |
740 | $title, |
741 | WikiPageConfigLoader::READ_UNCACHED |
742 | ); |
743 | if ( !is_array( $res ) ) { |
744 | // TODO: Maybe log the failure? |
745 | return []; |
746 | } |
747 | |
748 | $this->newcomerTasksConfig = $res; |
749 | return $res; |
750 | } |
751 | |
752 | /** |
753 | * Get config type from a form field name |
754 | * |
755 | * Form field names are always $configType-$configName, where |
756 | * $configType refers to the config page the variable is set in and |
757 | * $configName is the variable name. |
758 | * |
759 | * @param string $nameRaw |
760 | * @return string[] |
761 | */ |
762 | private function getPrefixAndName( string $nameRaw ): array { |
763 | return explode( '-', $nameRaw, 2 ); |
764 | } |
765 | |
766 | /** |
767 | * @inheritDoc |
768 | */ |
769 | protected function getFormFields() { |
770 | if ( $this->errorMsgKey !== null ) { |
771 | // Return an empty array when there is an error |
772 | return []; |
773 | } |
774 | |
775 | $descriptors = $this->getRawDescriptors(); |
776 | |
777 | // Add default values for geconfig variables |
778 | foreach ( $descriptors as $nameRaw => $descriptor ) { |
779 | [ $prefix, $name ] = $this->getPrefixAndName( $nameRaw ); |
780 | if ( strpos( $name, '-' ) !== false ) { |
781 | // Non-standard field, will be handled later in this method |
782 | continue; |
783 | } |
784 | |
785 | if ( $prefix === 'geconfig' ) { |
786 | $default = $this->getValueGeConfig( $name ); |
787 | if ( $default !== null ) { |
788 | $descriptors[$nameRaw]['default'] = $default; |
789 | } |
790 | } |
791 | } |
792 | |
793 | if ( LevelingUpManager::isEnabledForAnyone( $this->getConfig() ) ) { |
794 | $descriptors['geconfig-GELevelingUpKeepGoingNotificationThresholds-maximum']['default'] = |
795 | $this->growthWikiConfig->get( 'GELevelingUpKeepGoingNotificationThresholds' )[1]; |
796 | } |
797 | |
798 | // Add default values for newcomertasks variables |
799 | $newcomerTasksConfig = $this->getNewcomerTasksConfig(); |
800 | foreach ( $this->getDefaultDataForEnabledTaskTypes() as $taskType => $group ) { |
801 | $descriptors["newcomertasks-{$taskType}Disabled"]['default'] |
802 | = !empty( $newcomerTasksConfig[$taskType]['disabled'] ); |
803 | $descriptors["newcomertasks-{$taskType}Templates"]['default'] = implode( |
804 | "\n", |
805 | array_map( function ( $rawTitle ) { |
806 | return $this->titleFactory |
807 | ->newFromTextThrow( $rawTitle, NS_TEMPLATE ) |
808 | ->getPrefixedText(); |
809 | }, $newcomerTasksConfig[$taskType]['templates'] ?? [] ) |
810 | ); |
811 | $descriptors["newcomertasks-{$taskType}ExcludedTemplates"]['default'] = implode( |
812 | "\n", |
813 | array_map( function ( $rawTitle ) { |
814 | return $this->titleFactory |
815 | ->newFromTextThrow( $rawTitle, NS_TEMPLATE ) |
816 | ->getPrefixedText(); |
817 | }, $newcomerTasksConfig[$taskType]['excludedTemplates'] ?? [] ) |
818 | ); |
819 | $descriptors["newcomertasks-{$taskType}ExcludedCategories"]['default'] = implode( |
820 | "\n", |
821 | array_map( function ( $rawTitle ) { |
822 | return $this->titleFactory |
823 | ->newFromTextThrow( $rawTitle, NS_CATEGORY ) |
824 | ->getPrefixedText(); |
825 | }, $newcomerTasksConfig[$taskType]['excludedCategories'] ?? [] ) |
826 | ); |
827 | $descriptors["newcomertasks-{$taskType}Learnmore"]['default'] = |
828 | $newcomerTasksConfig[$taskType]['learnmore'] ?? ''; |
829 | |
830 | if ( $taskType === LinkRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
831 | $maxLinksDescriptorName = "newcomertasks-{$taskType}" . |
832 | ucfirst( LinkRecommendationTaskType::FIELD_MAX_LINKS_TO_SHOW_PER_TASK ); |
833 | $descriptors[$maxLinksDescriptorName]['default'] = |
834 | $newcomerTasksConfig[$taskType][LinkRecommendationTaskType::FIELD_MAX_LINKS_TO_SHOW_PER_TASK] ?? |
835 | $descriptors[$maxLinksDescriptorName]['default']; |
836 | $maxTasksDescriptorName = "newcomertasks-{$taskType}" . |
837 | ucfirst( LinkRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY ); |
838 | $descriptors[$maxTasksDescriptorName]['default'] = |
839 | $newcomerTasksConfig[$taskType][LinkRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY] ?? |
840 | $descriptors[$maxTasksDescriptorName]['default']; |
841 | $descriptors[$maxLinksDescriptorName]['min'] = |
842 | $newcomerTasksConfig[$taskType][LinkRecommendationTaskType::FIELD_MIN_LINKS_PER_TASK] ?? |
843 | $descriptors[$maxLinksDescriptorName]['min']; |
844 | $descriptors[$maxLinksDescriptorName]['max'] = |
845 | $newcomerTasksConfig[$taskType][LinkRecommendationTaskType::FIELD_MAX_LINKS_PER_TASK] ?? |
846 | $descriptors[$maxLinksDescriptorName]['max']; |
847 | |
848 | $excludeSectionsDescriptorName = "newcomertasks-{$taskType}" . |
849 | ucfirst( LinkRecommendationTaskType::FIELD_EXCLUDED_SECTIONS ); |
850 | $descriptors[$excludeSectionsDescriptorName]['default'] = implode( "\n", |
851 | $newcomerTasksConfig[$taskType][LinkRecommendationTaskType::FIELD_EXCLUDED_SECTIONS] ?? |
852 | $descriptors[$excludeSectionsDescriptorName]['default'] |
853 | ); |
854 | |
855 | // Ugly special-casing: if link-recommendations is soft-disabled, show it so |
856 | // configuration can be changed (in the future, once the special page supports that) |
857 | // but warn about it being disabled. |
858 | if ( $this->getConfig()->get( 'GELinkRecommendationsFrontendEnabled' ) === false ) { |
859 | $descriptors["newcomertasks-{$taskType}Disabled"] = [ |
860 | 'type' => 'info', |
861 | 'default' => new IconWidget( [ 'icon' => 'cancel' ] ) . ' ' |
862 | . $this->msg( 'growthexperiments-edit-config-newcomer-tasks-disabledinconfig' )->parse(), |
863 | 'raw' => true, |
864 | 'section' => 'newcomertasks', |
865 | ]; |
866 | } |
867 | } elseif ( $taskType === ImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
868 | $maxTasksDescriptorName = "newcomertasks-{$taskType}" . |
869 | ucfirst( ImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY ); |
870 | $descriptors[$maxTasksDescriptorName]['default'] = |
871 | $newcomerTasksConfig[$taskType][ImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY] ?? |
872 | $descriptors[$maxTasksDescriptorName]['default']; |
873 | } elseif ( $taskType === SectionImageRecommendationTaskTypeHandler::TASK_TYPE_ID ) { |
874 | $maxTasksDescriptorName = "newcomertasks-{$taskType}" . |
875 | ucfirst( SectionImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY ); |
876 | $descriptors[$maxTasksDescriptorName]['default'] = |
877 | $newcomerTasksConfig[$taskType][SectionImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY] ?? |
878 | $descriptors[$maxTasksDescriptorName]['default']; |
879 | } |
880 | } |
881 | |
882 | // Add default values for intro links |
883 | $linkValues = $this->growthWikiConfig->getWithFlags( |
884 | 'GEHomepageSuggestedEditsIntroLinks', |
885 | GrowthExperimentsMultiConfig::READ_UNCACHED |
886 | ); |
887 | foreach ( self::SUGGESTED_EDITS_INTRO_LINKS as $link ) { |
888 | $descriptors["geconfig-GEHomepageSuggestedEditsIntroLinks-$link"]['default'] = |
889 | $linkValues[$link]; |
890 | } |
891 | |
892 | // Add default values for help panel links |
893 | $helpPanelLinks = $this->growthWikiConfig->get( 'GEHelpPanelLinks' ); |
894 | foreach ( $helpPanelLinks as $i => $link ) { |
895 | if ( |
896 | isset( $descriptors["geconfig-GEHelpPanelLinks-$i-title"] ) && |
897 | isset( $descriptors["geconfig-GEHelpPanelLinks-$i-label"] ) |
898 | ) { |
899 | $descriptors["geconfig-GEHelpPanelLinks-$i-title"]['default'] = $link['title']; |
900 | $descriptors["geconfig-GEHelpPanelLinks-$i-label"]['default'] = $link['text']; |
901 | } |
902 | } |
903 | |
904 | // Add edit summary field, if user can write |
905 | if ( $this->userCanWrite ) { |
906 | $descriptors['summary'] = [ |
907 | 'type' => 'text', |
908 | 'label-message' => 'growthexperiments-edit-config-edit-summary', |
909 | ]; |
910 | } |
911 | return $descriptors; |
912 | } |
913 | |
914 | private function normalizeSuggestedEditsIntroLinks( array $data ): array { |
915 | $links = []; |
916 | foreach ( self::SUGGESTED_EDITS_INTRO_LINKS as $link ) { |
917 | $links[$link] = $data["geconfig-GEHomepageSuggestedEditsIntroLinks-$link"]; |
918 | } |
919 | return $links; |
920 | } |
921 | |
922 | private function normalizeHelpPanelLinks( array $data ): array { |
923 | $res = []; |
924 | // Right now, we support up to 5 help panel links |
925 | // If you want to change this, don't forget to update |
926 | // SpecialEditGrowthConfig::getRawDescriptors as well. |
927 | $supportedHelpPanelLinks = 5; |
928 | for ( $i = 0; $i < $supportedHelpPanelLinks; $i++ ) { |
929 | if ( |
930 | $data["geconfig-GEHelpPanelLinks-$i-title"] == '' || |
931 | $data["geconfig-GEHelpPanelLinks-$i-label"] == '' |
932 | ) { |
933 | continue; |
934 | } |
935 | |
936 | $linkId = null; |
937 | $title = $this->titleFactory->newFromText( $data["geconfig-GEHelpPanelLinks-$i-title"] ); |
938 | if ( $title !== null && $title->exists() && !$title->isExternal() ) { |
939 | $props = $this->pageProps->getProperties( $title, 'wikibase_item' ); |
940 | $pageId = $title->getId(); |
941 | if ( array_key_exists( $pageId, $props ) ) { |
942 | $linkId = $props[$pageId]; |
943 | } |
944 | } |
945 | $res[] = [ |
946 | 'title' => $data["geconfig-GEHelpPanelLinks-$i-title"], |
947 | 'text' => $data["geconfig-GEHelpPanelLinks-$i-label"], |
948 | 'id' => $linkId ?? $title->getPrefixedDBkey(), |
949 | ]; |
950 | } |
951 | return $res; |
952 | } |
953 | |
954 | /** |
955 | * Helper function that preprocesses submitted data |
956 | * |
957 | * This function: |
958 | * * normalizes namespaces into arrays |
959 | * * normalizes string true/false variables to actual booleans |
960 | * * ignores "complex fields" (fields having - in their name) and field for edit summary |
961 | * * splits variables by config type (one for each config page) |
962 | * |
963 | * @param array $data |
964 | * @return array |
965 | */ |
966 | private function preprocessSubmittedData( array $data ): array { |
967 | $dataToSave = []; |
968 | foreach ( $this->getFormFields() as $nameRaw => $descriptor ) { |
969 | if ( $nameRaw === 'summary' ) { |
970 | continue; |
971 | } |
972 | |
973 | [ $prefix, $name ] = $this->getPrefixAndName( $nameRaw ); |
974 | |
975 | if ( $descriptor['type'] === 'namespacesmultiselect' ) { |
976 | if ( $data[$nameRaw] === '' ) { |
977 | $data[$nameRaw] = []; |
978 | } else { |
979 | $data[$nameRaw] = array_map( |
980 | 'intval', |
981 | explode( "\n", $data[$nameRaw] ) |
982 | ); |
983 | } |
984 | } elseif ( $descriptor['type'] === 'int' ) { |
985 | $data[$nameRaw] = (int)$data[$nameRaw]; |
986 | } |
987 | |
988 | // Ignore fields with dashes except for newcomertasks, where task types |
989 | // can have a dash, e.g. 'link-recommendation' |
990 | if ( $prefix === 'newcomertasks' || strpos( $name, '-' ) === false ) { |
991 | $dataToSave[$prefix][$name] = $data[$nameRaw] ?? 'false'; |
992 | |
993 | // Basic normalization |
994 | if ( $dataToSave[$prefix][$name] === 'true' ) { |
995 | $dataToSave[$prefix][$name] = true; |
996 | } elseif ( $dataToSave[$prefix][$name] === 'false' ) { |
997 | $dataToSave[$prefix][$name] = false; |
998 | } |
999 | } |
1000 | } |
1001 | return $dataToSave; |
1002 | } |
1003 | |
1004 | /** |
1005 | * Normalize configuration used in NewcomerTasks.json config file |
1006 | * |
1007 | * This function converts form fields into array that's then stored |
1008 | * in the JSON file. |
1009 | * |
1010 | * @param array $data |
1011 | * @return array |
1012 | */ |
1013 | private function normalizeSuggestedEditsConfig( array $data ): array { |
1014 | $suggestedEditsConfig = $this->getNewcomerTasksConfig() |
1015 | // If a new task type was added since the on-wiki config page has last been updated, |
1016 | // we want that task type to be created the next time someone saves the page. |
1017 | + array_map( static function ( array $taskTypeData ) { |
1018 | return [ |
1019 | 'disabled' => false, |
1020 | 'group' => $taskTypeData['difficulty'], |
1021 | 'templates' => [], |
1022 | 'excludedTemplates' => [], |
1023 | 'excludedCategories' => [], |
1024 | 'type' => $taskTypeData['handler-id'], |
1025 | ]; |
1026 | }, $this->getDefaultDataForEnabledTaskTypes() ); |
1027 | |
1028 | foreach ( $this->getDefaultDataForEnabledTaskTypes() as $taskType => $taskTypeData ) { |
1029 | $templates = array_map( static function ( Title $title ) { |
1030 | return $title->getText(); |
1031 | }, $this->normalizeTitleList( $data["{$taskType}Templates"] ?? null ) ); |
1032 | if ( $templates === [] && |
1033 | !in_array( $taskType, NewcomerTasksValidator::SUGGESTED_EDITS_MACHINE_SUGGESTIONS_TASK_TYPES ) |
1034 | ) { |
1035 | // Do not save template-based tasks with no templates |
1036 | continue; |
1037 | } |
1038 | $excludedTemplates = array_map( static function ( Title $title ) { |
1039 | return $title->getText(); |
1040 | }, $this->normalizeTitleList( $data["{$taskType}ExcludedTemplates"] ?? null ) ); |
1041 | |
1042 | $excludedCategories = array_map( static function ( Title $title ) { |
1043 | return $title->getText(); |
1044 | }, $this->normalizeTitleList( $data["{$taskType}ExcludedCategories"] ?? null ) ); |
1045 | |
1046 | $suggestedEditsConfig[$taskType] = [ |
1047 | 'disabled' => (bool)$data["{$taskType}Disabled"], |
1048 | 'templates' => $templates, |
1049 | 'excludedTemplates' => $excludedTemplates, |
1050 | 'excludedCategories' => $excludedCategories, |
1051 | 'type' => $taskTypeData['handler-id'], |
1052 | ] + $suggestedEditsConfig[$taskType]; |
1053 | |
1054 | // Add learnmore link if specified |
1055 | if ( isset( $data["{$taskType}Learnmore"] ) ) { |
1056 | $suggestedEditsConfig[$taskType]['learnmore'] = $data["{$taskType}Learnmore"]; |
1057 | } else { |
1058 | unset( $suggestedEditsConfig[$taskType]['learnmore'] ); |
1059 | } |
1060 | |
1061 | // link-recommendation specific |
1062 | if ( isset( $data['link-recommendationMaximumLinksToShowPerTask'] ) ) { |
1063 | $suggestedEditsConfig['link-recommendation']['maximumLinksToShowPerTask'] = |
1064 | $data['link-recommendationMaximumLinksToShowPerTask']; |
1065 | } |
1066 | if ( isset( $data['link-recommendationMaxTasksPerDay'] ) ) { |
1067 | $suggestedEditsConfig['link-recommendation'][ |
1068 | LinkRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
1069 | ] = $data['link-recommendationMaxTasksPerDay']; |
1070 | } |
1071 | if ( isset( $data['link-recommendationExcludedSections'] ) ) { |
1072 | $suggestedEditsConfig['link-recommendation']['excludedSections'] = |
1073 | ( $data['link-recommendationExcludedSections'] === '' ) |
1074 | ? [] |
1075 | : explode( "\n", $data['link-recommendationExcludedSections'] ); |
1076 | } |
1077 | |
1078 | // image-recommendation specific |
1079 | if ( isset( $data['image-recommendationMaxTasksPerDay'] ) ) { |
1080 | $suggestedEditsConfig['image-recommendation'][ |
1081 | ImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
1082 | ] = $data['image-recommendationMaxTasksPerDay']; |
1083 | } |
1084 | if ( isset( $data['section-image-recommendationMaxTasksPerDay'] ) ) { |
1085 | $suggestedEditsConfig['section-image-recommendation'][ |
1086 | SectionImageRecommendationTaskType::FIELD_MAX_TASKS_PER_DAY |
1087 | ] = $data['section-image-recommendationMaxTasksPerDay']; |
1088 | } |
1089 | } |
1090 | |
1091 | return $suggestedEditsConfig; |
1092 | } |
1093 | |
1094 | /** |
1095 | * Returns the contents of NewcomerTasksValidator::SUGGESTED_EDITS_TASK_TYPES, excluding those |
1096 | * which have been disabled for this wiki via PHP configuration. |
1097 | * @return array[] |
1098 | */ |
1099 | public function getDefaultDataForEnabledTaskTypes(): array { |
1100 | $preferenceMap = [ |
1101 | LinkRecommendationTaskTypeHandler::TASK_TYPE_ID => 'GENewcomerTasksLinkRecommendationsEnabled', |
1102 | ImageRecommendationTaskTypeHandler::TASK_TYPE_ID => 'GENewcomerTasksImageRecommendationsEnabled', |
1103 | SectionImageRecommendationTaskTypeHandler::TASK_TYPE_ID => |
1104 | 'GENewcomerTasksSectionImageRecommendationsEnabled', |
1105 | ]; |
1106 | return array_filter( NewcomerTasksValidator::SUGGESTED_EDITS_TASK_TYPES, |
1107 | function ( $taskType ) use ( $preferenceMap ) { |
1108 | if ( !array_key_exists( $taskType, $preferenceMap ) ) { |
1109 | return true; |
1110 | } |
1111 | return $this->getConfig()->get( $preferenceMap[$taskType] ); |
1112 | }, ARRAY_FILTER_USE_KEY ); |
1113 | } |
1114 | |
1115 | /** |
1116 | * Helper method for normalizeSuggestedEditsConfig() |
1117 | * @param string|null $list |
1118 | * @return Title[] List of valid titles |
1119 | */ |
1120 | private function normalizeTitleList( ?string $list ) { |
1121 | if ( $list === null || $list === '' ) { |
1122 | return []; |
1123 | } |
1124 | return array_values( array_filter( array_map( function ( string $titleText ) { |
1125 | $title = $this->titleFactory->newFromText( $titleText ); |
1126 | if ( $title === null ) { |
1127 | return null; |
1128 | } |
1129 | return $title; |
1130 | }, explode( "\n", $list ) ) ) ); |
1131 | } |
1132 | |
1133 | /** |
1134 | * @inheritDoc |
1135 | */ |
1136 | public function onSubmit( array $data ) { |
1137 | $this->checkReadOnly(); |
1138 | |
1139 | // DO NOT rely on userCanWrite here, in case its value is wrong for some weird reason |
1140 | if ( !$this->getAuthority()->isAllowed( self::REQUIRED_RIGHT_TO_WRITE ) ) { |
1141 | throw new PermissionsError( self::REQUIRED_RIGHT_TO_WRITE ); |
1142 | } |
1143 | |
1144 | $dataToSave = $this->preprocessSubmittedData( $data ); |
1145 | |
1146 | $geconfigThresholds = $this->growthWikiConfig->get( 'GELevelingUpKeepGoingNotificationThresholds' ); |
1147 | $geconfigThresholds[1] = intval( $data['geconfig-GELevelingUpKeepGoingNotificationThresholds-maximum'] ); |
1148 | $dataToSave['geconfig']['GELevelingUpKeepGoingNotificationThresholds'] = $geconfigThresholds; |
1149 | |
1150 | // Normalize complex variables |
1151 | $dataToSave['geconfig']['GEHomepageSuggestedEditsIntroLinks'] = |
1152 | $this->normalizeSuggestedEditsIntroLinks( $data ); |
1153 | $dataToSave['geconfig']['GEHelpPanelLinks'] = $this->normalizeHelpPanelLinks( $data ); |
1154 | $dataToSave['geconfig']['GEInfoboxTemplates'] = array_map( static function ( Title $title ) { |
1155 | return $title->getPrefixedText(); |
1156 | }, $this->normalizeTitleList( $data['geconfig-GEInfoboxTemplates'] ?? null ) ); |
1157 | |
1158 | // Normalize suggested edits configuration |
1159 | $dataToSave['newcomertasks'] = $this->normalizeSuggestedEditsConfig( $dataToSave['newcomertasks'] ); |
1160 | |
1161 | // Start atomic section; we can end up editing multiple pages here, |
1162 | // with some edits failing and other succeeding. We want to either save everything, |
1163 | // or nothing. |
1164 | $dbw = $this->loadBalancer->getConnection( DB_PRIMARY ); |
1165 | $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); |
1166 | |
1167 | // Actually save the edits |
1168 | $status = Status::newGood(); |
1169 | foreach ( $dataToSave as $configType => $configData ) { |
1170 | $configWriter = $this->configWriterFactory |
1171 | ->newWikiPageConfigWriter( $this->configPages[$configType], $this->getUser() ); |
1172 | $configWriter->setVariables( $configData ); |
1173 | $status->merge( $configWriter->save( $data['summary'] ) ); |
1174 | } |
1175 | |
1176 | // End atomic section if all edits succeeded, cancel it otherwise |
1177 | if ( $status->isOK() ) { |
1178 | $dbw->endAtomic( __METHOD__ ); |
1179 | } else { |
1180 | $dbw->cancelAtomic( __METHOD__ ); |
1181 | } |
1182 | |
1183 | $this->eventLogger->logAction( SpecialEditGrowthConfigLogger::ACTION_SAVE, $this->getAuthority() ); |
1184 | return $status; |
1185 | } |
1186 | |
1187 | /** |
1188 | * @inheritDoc |
1189 | */ |
1190 | public function onSuccess() { |
1191 | $out = $this->getOutput(); |
1192 | |
1193 | // Add success message |
1194 | $out->addWikiMsg( 'growthexperiments-edit-config-config-changed' ); |
1195 | $out->addWikiMsg( 'growthexperiments-edit-config-return-to-form' ); |
1196 | |
1197 | // Ask for feedback |
1198 | $out->addHTML( $this->getFeedbackHtml() ); |
1199 | } |
1200 | |
1201 | /** |
1202 | * Add feedback CTA to the output |
1203 | * |
1204 | * @return string HTML to add to the output |
1205 | */ |
1206 | private function getFeedbackHtml(): string { |
1207 | $this->getOutput()->addModuleStyles( 'oojs-ui.styles.icons-interactions' ); |
1208 | return Html::rawElement( 'div', [], implode( "\n", [ |
1209 | Html::rawElement( |
1210 | 'h3', |
1211 | [], |
1212 | $this->msg( 'growthexperiments-edit-config-feedback-headline' ) |
1213 | ), |
1214 | new ButtonWidget( [ |
1215 | 'icon' => 'feedback', |
1216 | 'label' => $this->msg( 'growthexperiments-edit-config-feedback-cta' ), |
1217 | 'href' => 'https://www.mediawiki.org/wiki/Talk:Growth', |
1218 | 'flags' => [ 'primary', 'progressive' ] |
1219 | ] ) |
1220 | ] ) ); |
1221 | } |
1222 | } |