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