Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 81 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
VisualEditorHooks | |
0.00% |
0 / 81 |
|
0.00% |
0 / 5 |
702 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onVisualEditorApiVisualEditorEditPreSave | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
onVisualEditorApiVisualEditorEditPostSave | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
72 | |||
onAPIGetAllowedParams | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
getDataFromApiRequest | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments; |
4 | |
5 | use GrowthExperiments\HomepageModules\SuggestedEdits; |
6 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader; |
7 | use GrowthExperiments\NewcomerTasks\TaskType\StructuredTaskTypeHandler; |
8 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
9 | use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandler; |
10 | use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandlerRegistry; |
11 | use MediaWiki\Api\ApiBase; |
12 | use MediaWiki\Api\Hook\APIGetAllowedParamsHook; |
13 | use MediaWiki\Extension\VisualEditor\ApiVisualEditorEdit; |
14 | use MediaWiki\Extension\VisualEditor\VisualEditorApiVisualEditorEditPostSaveHook; |
15 | use MediaWiki\Extension\VisualEditor\VisualEditorApiVisualEditorEditPreSaveHook; |
16 | use MediaWiki\Page\ProperPageIdentity; |
17 | use MediaWiki\Status\Status; |
18 | use MediaWiki\Title\TitleFactory; |
19 | use MediaWiki\User\UserIdentity; |
20 | use MediaWiki\User\UserIdentityUtils; |
21 | use OutOfBoundsException; |
22 | use UnexpectedValueException; |
23 | use Wikimedia\Stats\PrefixingStatsdDataFactoryProxy; |
24 | |
25 | /** |
26 | * Hook handlers for hooks defined in VisualEditor. |
27 | */ |
28 | class VisualEditorHooks implements |
29 | APIGetAllowedParamsHook, |
30 | VisualEditorApiVisualEditorEditPreSaveHook, |
31 | VisualEditorApiVisualEditorEditPostSaveHook |
32 | { |
33 | |
34 | /** Prefix used for the VisualEditor API's plugin parameter. */ |
35 | public const PLUGIN_PREFIX = 'ge-task-'; |
36 | |
37 | /** @var TitleFactory */ |
38 | private $titleFactory; |
39 | /** @var ConfigurationLoader */ |
40 | private $configurationLoader; |
41 | /** @var TaskTypeHandlerRegistry */ |
42 | private $taskTypeHandlerRegistry; |
43 | /** @var PrefixingStatsdDataFactoryProxy */ |
44 | private $perDbNameStatsdDataFactory; |
45 | /** @var UserIdentityUtils */ |
46 | private $userIdentityUtils; |
47 | |
48 | /** |
49 | * @param TitleFactory $titleFactory |
50 | * @param ConfigurationLoader $configurationLoader |
51 | * @param TaskTypeHandlerRegistry $taskTypeHandlerRegistry |
52 | * @param PrefixingStatsdDataFactoryProxy $perDbNameStatsdDataFactory |
53 | * @param UserIdentityUtils $userIdentityUtils |
54 | */ |
55 | public function __construct( |
56 | TitleFactory $titleFactory, |
57 | ConfigurationLoader $configurationLoader, |
58 | TaskTypeHandlerRegistry $taskTypeHandlerRegistry, |
59 | PrefixingStatsdDataFactoryProxy $perDbNameStatsdDataFactory, |
60 | UserIdentityUtils $userIdentityUtils |
61 | ) { |
62 | $this->titleFactory = $titleFactory; |
63 | $this->configurationLoader = $configurationLoader; |
64 | $this->taskTypeHandlerRegistry = $taskTypeHandlerRegistry; |
65 | $this->perDbNameStatsdDataFactory = $perDbNameStatsdDataFactory; |
66 | $this->userIdentityUtils = $userIdentityUtils; |
67 | } |
68 | |
69 | /** @inheritDoc */ |
70 | public function onVisualEditorApiVisualEditorEditPreSave( |
71 | ProperPageIdentity $page, |
72 | UserIdentity $user, |
73 | string $wikitext, |
74 | array &$params, |
75 | array $pluginData, |
76 | array &$apiResponse |
77 | ) { |
78 | // This is going to run on every edit and not in a deferred update, so at least filter |
79 | // by authenticated users to make this slightly faster for anons. |
80 | if ( !$this->userIdentityUtils->isNamed( $user ) ) { |
81 | return; |
82 | } |
83 | /** @var ?TaskTypeHandler $taskTypeHandler */ |
84 | [ $data, $taskTypeHandler, $taskType ] = $this->getDataFromApiRequest( $pluginData ); |
85 | if ( !$data || !$taskType ) { |
86 | // Not an edit we are interested in looking at. |
87 | return; |
88 | } |
89 | $title = $this->titleFactory->castFromPageIdentity( $page ); |
90 | if ( !$title ) { |
91 | // Something weird has happened; let the save attempt go through because |
92 | // presumably later an exception will be thrown and that can be dealt |
93 | // with by VisualEditor. |
94 | return; |
95 | } |
96 | $status = $taskTypeHandler->getSubmissionHandler()->validate( |
97 | $taskType, |
98 | $title->toPageIdentity(), |
99 | $user, |
100 | $params['oldid'], |
101 | $data |
102 | ); |
103 | if ( !$status->isGood() ) { |
104 | $message = Status::wrap( $status )->getMessage(); |
105 | $apiResponse['message'] = array_merge( [ $message->getKey() ], $message->getParams() ); |
106 | Util::logStatus( $status ); |
107 | return false; |
108 | } |
109 | } |
110 | |
111 | /** @inheritDoc */ |
112 | public function onVisualEditorApiVisualEditorEditPostSave( |
113 | ProperPageIdentity $page, |
114 | UserIdentity $user, |
115 | string $wikitext, |
116 | array $params, |
117 | array $pluginData, |
118 | array $saveResult, |
119 | array &$apiResponse |
120 | ): void { |
121 | if ( $apiResponse['result'] !== 'success' ) { |
122 | return; |
123 | } |
124 | // This is going to run on every edit and not in a deferred update, so at least filter |
125 | // by authenticated users to make this slightly faster for anons. |
126 | if ( !$this->userIdentityUtils->isNamed( $user ) ) { |
127 | return; |
128 | } |
129 | /** @var ?TaskTypeHandler $taskTypeHandler */ |
130 | /** @var ?TaskType $taskType */ |
131 | [ $data, $taskTypeHandler, $taskType ] = $this->getDataFromApiRequest( $pluginData ); |
132 | if ( !$data || !$taskType ) { |
133 | return; |
134 | } |
135 | $title = $this->titleFactory->castFromPageIdentity( $page ); |
136 | if ( !$title ) { |
137 | throw new UnexpectedValueException( 'Unable to get Title from PageIdentity' ); |
138 | } |
139 | |
140 | $newRevId = $saveResult['edit']['newrevid'] ?? null; |
141 | |
142 | $status = $taskTypeHandler->getSubmissionHandler()->handle( |
143 | $taskType, |
144 | $title->toPageIdentity(), |
145 | $user, |
146 | $params['oldid'], |
147 | $newRevId, |
148 | $data |
149 | ); |
150 | if ( $status->isGood() ) { |
151 | $apiResponse['gelogid'] = $status->getValue()['logId'] ?? null; |
152 | $apiResponse['gewarnings'][] = $status->getValue()['warnings'] ?? ''; |
153 | if ( $newRevId ) { |
154 | $this->perDbNameStatsdDataFactory->increment( |
155 | 'GrowthExperiments.NewcomerTask.' . $taskType->getId() . '.Save' |
156 | ); |
157 | } |
158 | } else { |
159 | // FIXME expose error formatter to hook so this can be handled better |
160 | $errorMessage = Status::wrap( $status )->getWikiText(); |
161 | $apiResponse['errors'][] = $errorMessage; |
162 | Util::logStatus( $status ); |
163 | } |
164 | } |
165 | |
166 | /** @inheritDoc */ |
167 | public function onAPIGetAllowedParams( $module, &$params, $flags ) { |
168 | if ( $module instanceof ApiVisualEditorEdit |
169 | && ( $flags & ApiBase::GET_VALUES_FOR_HELP ) |
170 | && SuggestedEdits::isEnabled( $module->getContext()->getConfig() ) |
171 | ) { |
172 | $taskTypes = $this->configurationLoader->getTaskTypes(); |
173 | foreach ( $taskTypes as $taskTypeId => $taskType ) { |
174 | $taskTypeHandler = $this->taskTypeHandlerRegistry->getByTaskType( $taskType ); |
175 | if ( $taskTypeHandler instanceof StructuredTaskTypeHandler ) { |
176 | $paramValue = self::PLUGIN_PREFIX . $taskTypeId; |
177 | $params['plugins'][ApiBase::PARAM_HELP_MSG_PER_VALUE][$paramValue] = [ |
178 | "apihelp-visualeditoredit-paramvalue-plugins-$paramValue", |
179 | ]; |
180 | $params['data-{plugin}'][ApiBase::PARAM_HELP_MSG_APPEND][$paramValue] = [ |
181 | "apihelp-visualeditoredit-append-data-plugin-$paramValue", |
182 | $taskTypeHandler->getSubmitDataFormatMessage( $taskType, $module->getContext() ), |
183 | ]; |
184 | } |
185 | } |
186 | } |
187 | } |
188 | |
189 | /** |
190 | * Extract the data sent by the frontend structured task logic from the API request. |
191 | * @param array $pluginData |
192 | * @return array [ JSON data from frontend, TaskTypeHandler, task type ID ] or [ null, null, null ] |
193 | * @phan-return array{0:?array,1:?TaskTypeHandler,2:?TaskType} |
194 | */ |
195 | private function getDataFromApiRequest( array $pluginData ): array { |
196 | // Fast-track the common case of a non-Growth-related save - getTaskTypes() is not free. |
197 | if ( !$pluginData ) { |
198 | return [ null, null, null ]; |
199 | } |
200 | |
201 | $taskTypes = $this->configurationLoader->getTaskTypes(); |
202 | foreach ( $taskTypes as $taskTypeId => $taskType ) { |
203 | $data = $pluginData[ self::PLUGIN_PREFIX . $taskTypeId ] ?? null; |
204 | if ( $data ) { |
205 | $data = json_decode( $data, true ) ?? []; |
206 | try { |
207 | $taskTypeHandler = $this->taskTypeHandlerRegistry->getByTaskType( $taskType ); |
208 | } catch ( OutOfBoundsException $e ) { |
209 | // Probably some sort of hand-crafted fake API request. Ignore it. |
210 | continue; |
211 | } |
212 | |
213 | return [ $data, $taskTypeHandler, $taskType ]; |
214 | } |
215 | } |
216 | return [ null, null, null ]; |
217 | } |
218 | |
219 | } |