Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
45.64% |
110 / 241 |
|
13.33% |
4 / 30 |
CRAP | |
0.00% |
0 / 1 |
QuestionPoster | |
45.64% |
110 / 241 |
|
13.33% |
4 / 30 |
765.60 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
setPostOnTop | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRelevantTitle | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getWikitextLinkTarget | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
loadExistingQuestions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
submit | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
6.20 | |||
getTargetContentModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
submitWikitext | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
30 | |||
submitStructuredDiscussions | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
getNumberedSectionHeaderIfDuplicatesExist | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
checkPermissions | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
getTag | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
makeWikitextContent | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
getPageUpdater | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addSignature | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
validateRelevantTitle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getResultUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRevisionId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFirstEdit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSectionHeaderTemplate | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setResultUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setSectionHeader | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getSectionHeader | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPostedOnTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFormattedPostedOnTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getTargetTitle | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
getDirectTargetTitle | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getContext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQuestionStoragePref | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
checkUserPermissions | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
runEditFilterMergedContentHook | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
3.01 | |||
getBody | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkContent | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
saveNewQuestion | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\HelpPanel\QuestionPoster; |
4 | |
5 | use Flow\Container; |
6 | use GrowthExperiments\HelpPanel\QuestionRecord; |
7 | use GrowthExperiments\HelpPanel\QuestionStoreFactory; |
8 | use GrowthExperiments\Hooks\HookRunner; |
9 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
10 | use MediaWiki\CommentStore\CommentStoreComment; |
11 | use MediaWiki\Config\Config; |
12 | use MediaWiki\Content\Content; |
13 | use MediaWiki\Content\WikitextContent; |
14 | use MediaWiki\Context\DerivativeContext; |
15 | use MediaWiki\Context\IContextSource; |
16 | use MediaWiki\MediaWikiServices; |
17 | use MediaWiki\Page\WikiPageFactory; |
18 | use MediaWiki\Permissions\PermissionManager; |
19 | use MediaWiki\Revision\SlotRecord; |
20 | use MediaWiki\Status\Status; |
21 | use MediaWiki\Storage\PageUpdater; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\Title\TitleFactory; |
24 | use RecentChange; |
25 | use RuntimeException; |
26 | use UserNotLoggedIn; |
27 | use Wikimedia\Stats\PrefixingStatsdDataFactoryProxy; |
28 | |
29 | /** |
30 | * Base class for sending messages containing user questions to some target page. |
31 | */ |
32 | abstract class QuestionPoster { |
33 | |
34 | /** |
35 | * @var WikiPageFactory |
36 | */ |
37 | private $wikiPageFactory; |
38 | |
39 | /** |
40 | * @var TitleFactory |
41 | */ |
42 | private $titleFactory; |
43 | |
44 | /** |
45 | * @var PermissionManager |
46 | */ |
47 | private $permissionManager; |
48 | |
49 | /** |
50 | * @var bool |
51 | */ |
52 | private $postOnTop = false; |
53 | |
54 | /** |
55 | * @var IContextSource |
56 | */ |
57 | private $context; |
58 | |
59 | /** |
60 | * @var bool |
61 | */ |
62 | private $isFirstEdit; |
63 | |
64 | /** |
65 | * @var Config |
66 | */ |
67 | private $config; |
68 | |
69 | /** |
70 | * @var Title |
71 | */ |
72 | private $targetTitle; |
73 | |
74 | /** |
75 | * @var string |
76 | */ |
77 | private $resultUrl; |
78 | |
79 | /** |
80 | * @var PageUpdater |
81 | */ |
82 | protected $pageUpdater; |
83 | |
84 | /** |
85 | * @var mixed |
86 | */ |
87 | private $revisionId; |
88 | |
89 | /** |
90 | * @var string |
91 | */ |
92 | private $relevantTitleRaw; |
93 | |
94 | /** |
95 | * @var Title|null |
96 | */ |
97 | private $relevantTitle; |
98 | |
99 | /** |
100 | * @var string |
101 | */ |
102 | private $postedOnTimestamp; |
103 | |
104 | /** |
105 | * @var QuestionRecord[] |
106 | */ |
107 | private $existingQuestionsByUser; |
108 | |
109 | /** |
110 | * @var string |
111 | */ |
112 | private $body; |
113 | |
114 | /** |
115 | * @var string |
116 | */ |
117 | private $sectionHeader; |
118 | |
119 | /** |
120 | * @var PrefixingStatsdDataFactoryProxy |
121 | */ |
122 | private $perDbNameStatsdDataFactory; |
123 | |
124 | private bool $confirmEditInstalled = false; |
125 | private bool $flowInstalled = false; |
126 | |
127 | /** |
128 | * @param WikiPageFactory $wikiPageFactory |
129 | * @param TitleFactory $titleFactory |
130 | * @param PermissionManager $permissionManager |
131 | * @param StatsdDataFactoryInterface $perDbNameStatsdDataFactory |
132 | * @param bool $confirmEditInstalled |
133 | * @param bool $flowInstalled |
134 | * @param IContextSource $context |
135 | * @param string $body |
136 | * @param string $relevantTitleRaw |
137 | * @throws UserNotLoggedIn |
138 | */ |
139 | public function __construct( |
140 | WikiPageFactory $wikiPageFactory, |
141 | TitleFactory $titleFactory, |
142 | PermissionManager $permissionManager, |
143 | StatsdDataFactoryInterface $perDbNameStatsdDataFactory, |
144 | bool $confirmEditInstalled, |
145 | bool $flowInstalled, |
146 | IContextSource $context, |
147 | $body, |
148 | $relevantTitleRaw = '' |
149 | ) { |
150 | $this->wikiPageFactory = $wikiPageFactory; |
151 | $this->titleFactory = $titleFactory; |
152 | $this->permissionManager = $permissionManager; |
153 | $this->context = $context; |
154 | $this->relevantTitleRaw = $relevantTitleRaw; |
155 | if ( !$this->getContext()->getUser()->isNamed() ) { |
156 | throw new UserNotLoggedIn(); |
157 | } |
158 | $this->config = $this->getContext()->getConfig(); |
159 | $this->isFirstEdit = ( $this->getContext()->getUser()->getEditCount() === 0 ); |
160 | $this->targetTitle = $this->getTargetTitle(); |
161 | $page = $wikiPageFactory->newFromTitle( $this->targetTitle ); |
162 | $this->pageUpdater = $page->newPageUpdater( $this->getContext()->getUser() ); |
163 | $this->body = trim( $body ); |
164 | $this->perDbNameStatsdDataFactory = $perDbNameStatsdDataFactory; |
165 | $this->confirmEditInstalled = $confirmEditInstalled; |
166 | $this->flowInstalled = $flowInstalled; |
167 | } |
168 | |
169 | /** |
170 | * Whether to post on top of the help desk (as opposed to the bottom). Defaults to false. |
171 | * Only affects wikitext pages. |
172 | * @param bool $postOnTop |
173 | */ |
174 | public function setPostOnTop( bool $postOnTop ): void { |
175 | $this->postOnTop = $postOnTop; |
176 | } |
177 | |
178 | /** |
179 | * Return relevant title, if it exists. |
180 | * @return Title|null |
181 | */ |
182 | public function getRelevantTitle(): ?Title { |
183 | if ( !$this->relevantTitleRaw ) { |
184 | return null; |
185 | } |
186 | |
187 | if ( $this->relevantTitle === null ) { |
188 | $this->relevantTitle = $this->titleFactory->newFromText( |
189 | $this->relevantTitleRaw |
190 | ); |
191 | } |
192 | return $this->relevantTitle; |
193 | } |
194 | |
195 | /** |
196 | * Return wikitext link target suitable for usage in [[internal linking]] |
197 | * |
198 | * This returns [[$title->getPrefixedText()]] for most pages, and |
199 | * [[:$title->getPrefixedText()]] for files and categories. |
200 | * |
201 | * @return string |
202 | */ |
203 | public function getWikitextLinkTarget(): string { |
204 | $title = $this->getRelevantTitle(); |
205 | if ( !$title ) { |
206 | return ''; |
207 | } |
208 | |
209 | if ( in_array( $title->getNamespace(), [ NS_FILE, NS_CATEGORY ], true ) ) { |
210 | return ':' . $title->getPrefixedText(); |
211 | } else { |
212 | return $title->getPrefixedText(); |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Load the current user's existing questions. |
218 | */ |
219 | protected function loadExistingQuestions() { |
220 | $questionStore = QuestionStoreFactory::newFromContextAndStorage( |
221 | $this->getContext(), |
222 | $this->getQuestionStoragePref() |
223 | ); |
224 | $this->existingQuestionsByUser = $questionStore->loadQuestions(); |
225 | } |
226 | |
227 | /** |
228 | * @return Status |
229 | */ |
230 | public function submit() { |
231 | $this->loadExistingQuestions(); |
232 | |
233 | // Do not let captcha to stop us |
234 | if ( $this->confirmEditInstalled ) { |
235 | $scope = $this->permissionManager->addTemporaryUserRights( |
236 | $this->getContext()->getUser(), |
237 | 'skipcaptcha' |
238 | ); |
239 | } |
240 | |
241 | $this->postedOnTimestamp = wfTimestamp(); |
242 | $this->setSectionHeader(); |
243 | |
244 | $contentModel = $this->getTargetContentModel(); |
245 | if ( $contentModel === CONTENT_MODEL_WIKITEXT ) { |
246 | $status = $this->submitWikitext(); |
247 | } elseif ( $this->flowInstalled && $contentModel === CONTENT_MODEL_FLOW_BOARD ) { |
248 | $status = $this->submitStructuredDiscussions(); |
249 | } else { |
250 | throw new RuntimeException( "Content model $contentModel is not supported." ); |
251 | } |
252 | |
253 | if ( $status->isGood() ) { |
254 | $this->saveNewQuestion(); |
255 | } |
256 | |
257 | return $status; |
258 | } |
259 | |
260 | /** |
261 | * @return string Content model of the target page. One of the CONTENT_MODEL_* constants. |
262 | */ |
263 | protected function getTargetContentModel() { |
264 | return $this->targetTitle->getContentModel(); |
265 | } |
266 | |
267 | /** |
268 | * @return Status |
269 | */ |
270 | private function submitWikitext() { |
271 | $content = $this->makeWikitextContent(); |
272 | |
273 | $contentStatus = $this->checkContent( $content ); |
274 | if ( !$contentStatus->isGood() ) { |
275 | return $contentStatus; |
276 | } |
277 | $permissionStatus = $this->checkPermissions( $content ); |
278 | if ( !$permissionStatus->isGood() ) { |
279 | return $permissionStatus; |
280 | } |
281 | |
282 | $tag = $this->getTag(); |
283 | $this->getPageUpdater()->addTag( $tag ); |
284 | $this->getPageUpdater()->setContent( SlotRecord::MAIN, $content ); |
285 | if ( $this->getContext()->getAuthority()->authorizeWrite( 'autopatrol', $this->targetTitle ) ) { |
286 | $this->getPageUpdater()->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED ); |
287 | } |
288 | $newRev = $this->getPageUpdater()->saveRevision( |
289 | CommentStoreComment::newUnsavedComment( |
290 | $this->getContext() |
291 | ->msg( 'newsectionsummary' ) |
292 | ->params( |
293 | MediaWikiServices::getInstance() |
294 | ->getParserFactory() |
295 | ->create() |
296 | ->stripSectionName( $this->getSectionHeader() ) |
297 | ) |
298 | ->text() |
299 | ) |
300 | ); |
301 | if ( !$this->getPageUpdater()->getStatus()->isGood() ) { |
302 | return $this->getPageUpdater()->getStatus(); |
303 | } |
304 | |
305 | $this->revisionId = $newRev->getId(); |
306 | $fragment = MediaWikiServices::getInstance() |
307 | ->getParser() |
308 | ->guessSectionNameFromWikiText( $this->getSectionHeader() ); |
309 | |
310 | // NOTE: Don't call setFragment() on the original Title, that may corrupt the internal |
311 | // cache of Title objects. |
312 | $target = Title::makeTitle( |
313 | $this->targetTitle->getNamespace(), |
314 | $this->targetTitle->getDBkey(), |
315 | $fragment |
316 | ); |
317 | $this->setResultUrl( $target->getLinkURL() ); |
318 | $tagId = str_replace( ' ', '_', $tag ); |
319 | $this->perDbNameStatsdDataFactory->increment( 'GrowthExperiments.QuestionPoster.' . $tagId ); |
320 | |
321 | return Status::newGood(); |
322 | } |
323 | |
324 | /** |
325 | * @return Status |
326 | */ |
327 | private function submitStructuredDiscussions() { |
328 | $workflowLoaderFactory = Container::get( 'factory.loader.workflow' ); |
329 | // TODO: Add statsd instrumentation after T297709 is done. |
330 | $loader = $workflowLoaderFactory->createWorkflowLoader( $this->targetTitle ); |
331 | $blocksToCommit = $loader->handleSubmit( |
332 | $this->getContext(), |
333 | 'new-topic', |
334 | [ |
335 | 'topiclist' => [ |
336 | 'topic' => $this->getSectionHeader(), |
337 | 'content' => $this->getBody(), |
338 | 'format' => 'wikitext', |
339 | ], |
340 | ] |
341 | ); |
342 | |
343 | $status = Status::newGood(); |
344 | foreach ( $loader->getBlocks() as $block ) { |
345 | if ( $block->hasErrors() ) { |
346 | $errors = $block->getErrors(); |
347 | foreach ( $errors as $errorKey ) { |
348 | $status->fatal( $block->getErrorMessage( $errorKey ) ); |
349 | } |
350 | } |
351 | } |
352 | if ( !$status->isOK() ) { |
353 | return $status; |
354 | } |
355 | |
356 | $commitMetadata = $loader->commit( $blocksToCommit ); |
357 | |
358 | $topicTitle = Title::newFromText( $commitMetadata['topiclist']['topic-page'] ); |
359 | $this->setResultUrl( $topicTitle->getLinkURL() ); |
360 | $this->revisionId = $commitMetadata['topiclist']['topic-id']->getAlphadecimal(); |
361 | |
362 | return Status::newGood(); |
363 | } |
364 | |
365 | private function getNumberedSectionHeaderIfDuplicatesExist( $sectionHeader ) { |
366 | $sectionHeaders = array_map( |
367 | static function ( QuestionRecord $questionRecord ) { |
368 | return $questionRecord->getSectionHeader(); |
369 | }, |
370 | $this->existingQuestionsByUser |
371 | ); |
372 | $counter = 1; |
373 | while ( in_array( $counter === 1 ? $sectionHeader : "$sectionHeader ($counter)", |
374 | $sectionHeaders ) ) { |
375 | $counter++; |
376 | } |
377 | return $counter === 1 ? $sectionHeader : $sectionHeader . ' (' . $counter . ')'; |
378 | } |
379 | |
380 | /** |
381 | * @param Content $content |
382 | * @return Status |
383 | */ |
384 | protected function checkPermissions( $content ) { |
385 | $userPermissionStatus = $this->checkUserPermissions(); |
386 | if ( !$userPermissionStatus->isGood() ) { |
387 | return $userPermissionStatus; |
388 | } |
389 | $editFilterMergedContentHookStatus = $this->runEditFilterMergedContentHook( |
390 | $content, |
391 | $this->getSectionHeaderTemplate() |
392 | ); |
393 | if ( !$editFilterMergedContentHookStatus->isGood() ) { |
394 | return $editFilterMergedContentHookStatus; |
395 | } |
396 | return Status::newGood(); |
397 | } |
398 | |
399 | /** |
400 | * The tag to add to the edit. |
401 | */ |
402 | abstract protected function getTag(); |
403 | |
404 | /** |
405 | * Create a Content object with the header and question text provided by the user. |
406 | * |
407 | * @return Content|null |
408 | */ |
409 | protected function makeWikitextContent() { |
410 | $wikitextContent = new WikitextContent( |
411 | $this->addSignature( $this->getBody() ) |
412 | ); |
413 | $header = $this->getSectionHeader(); |
414 | $parent = $this->getPageUpdater()->grabParentRevision(); |
415 | if ( !$parent ) { |
416 | return $wikitextContent->addSectionHeader( $header ); |
417 | } |
418 | $existingContent = $parent->getContent( SlotRecord::MAIN ); |
419 | if ( !$existingContent ) { |
420 | return null; |
421 | } |
422 | |
423 | if ( $this->postOnTop ) { |
424 | $section1 = $existingContent->getSection( 1 ); |
425 | if ( $section1 ) { |
426 | // Prepend to section 1 to post on top without disturbing top-of-the-page templates |
427 | return $existingContent->replaceSection( 1, |
428 | $wikitextContent->replaceSection( 'new', $section1 )->addSectionHeader( $header ) ); |
429 | } |
430 | // No sections on the page - just post on bottom. |
431 | } |
432 | return $existingContent->replaceSection( |
433 | 'new', |
434 | $wikitextContent, |
435 | $header |
436 | ); |
437 | } |
438 | |
439 | /** |
440 | * @return PageUpdater |
441 | */ |
442 | protected function getPageUpdater() { |
443 | return $this->pageUpdater; |
444 | } |
445 | |
446 | /** |
447 | * Add signature unless already set. |
448 | * |
449 | * @param string $body |
450 | * @return string |
451 | */ |
452 | private function addSignature( $body ) { |
453 | if ( strpos( $body, '~~~~' ) === false ) { |
454 | $body .= " --~~~~"; |
455 | } |
456 | return $body; |
457 | } |
458 | |
459 | /** |
460 | * @return Status |
461 | */ |
462 | public function validateRelevantTitle() { |
463 | $title = $this->getRelevantTitle(); |
464 | return $title && $title->isValid() ? |
465 | Status::newGood() : |
466 | Status::newFatal( 'growthexperiments-help-panel-questionposter-invalid-title' ); |
467 | } |
468 | |
469 | /** |
470 | * @return string |
471 | */ |
472 | public function getResultUrl() { |
473 | return $this->resultUrl; |
474 | } |
475 | |
476 | /** |
477 | * @return int |
478 | */ |
479 | public function getRevisionId() { |
480 | return $this->revisionId; |
481 | } |
482 | |
483 | /** |
484 | * @return bool |
485 | */ |
486 | public function isFirstEdit() { |
487 | return $this->isFirstEdit; |
488 | } |
489 | |
490 | /** |
491 | * Get the section header template for the question posted by the user. |
492 | * |
493 | * This method is used for generating the comment summary as well as the |
494 | * section header in the edit. |
495 | * |
496 | * @return string |
497 | */ |
498 | abstract protected function getSectionHeaderTemplate(); |
499 | |
500 | /** |
501 | * Set the result URL to go directly to the newly created question. |
502 | * |
503 | * @param string $resultUrl |
504 | */ |
505 | private function setResultUrl( $resultUrl ) { |
506 | $this->resultUrl = $resultUrl; |
507 | } |
508 | |
509 | /** |
510 | * Set the section header with a timestamp (wikitext only) and number. |
511 | * |
512 | * THe number is appended for flow posts. For wikitext posts, a number is appended |
513 | * only if duplicate headers exist, which can happen when questions |
514 | * are posted within the same minute. |
515 | */ |
516 | protected function setSectionHeader() { |
517 | $this->sectionHeader = $this->getSectionHeaderTemplate(); |
518 | // If wikitext, override the section header to include the timestamp. |
519 | if ( $this->getTargetContentModel() === CONTENT_MODEL_WIKITEXT ) { |
520 | $this->sectionHeader .= ' ' . $this->getContext() |
521 | ->msg( 'parentheses' ) |
522 | ->plaintextParams( $this->getFormattedPostedOnTimestamp() ) |
523 | ->inContentLanguage() |
524 | ->escaped(); |
525 | } |
526 | $this->sectionHeader = $this->getNumberedSectionHeaderIfDuplicatesExist( |
527 | $this->sectionHeader |
528 | ); |
529 | } |
530 | |
531 | /** |
532 | * @return string |
533 | */ |
534 | private function getSectionHeader() { |
535 | return $this->sectionHeader; |
536 | } |
537 | |
538 | /** |
539 | * @return string |
540 | */ |
541 | private function getPostedOnTimestamp() { |
542 | return $this->postedOnTimestamp; |
543 | } |
544 | |
545 | /** |
546 | * Timezone adjustment, site default format, and site default time zone are used for formatting. |
547 | * @return string |
548 | */ |
549 | private function getFormattedPostedOnTimestamp() { |
550 | return MediaWikiServices::getInstance()->getContentLanguage() |
551 | ->timeanddate( $this->getPostedOnTimestamp(), true, false, '' ); |
552 | } |
553 | |
554 | /** |
555 | * @return Title The page where the question should be posted. |
556 | */ |
557 | protected function getTargetTitle(): Title { |
558 | $title = $this->getDirectTargetTitle(); |
559 | if ( $title->isRedirect() ) { |
560 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
561 | return $page->getRedirectTarget(); |
562 | } |
563 | return $title; |
564 | } |
565 | |
566 | /** |
567 | * @return Title The page where the question should be posted (barring redirects). |
568 | */ |
569 | abstract protected function getDirectTargetTitle(); |
570 | |
571 | /** |
572 | * @return IContextSource |
573 | */ |
574 | final protected function getContext() { |
575 | return $this->context; |
576 | } |
577 | |
578 | /** |
579 | * The preference name where the posted question will be stored. |
580 | * |
581 | * @return string |
582 | */ |
583 | abstract protected function getQuestionStoragePref(); |
584 | |
585 | /** |
586 | * @return Status |
587 | * @throws \Exception |
588 | */ |
589 | protected function checkUserPermissions() { |
590 | $errors = $this->permissionManager->getPermissionErrors( |
591 | 'edit', |
592 | $this->getContext()->getUser(), |
593 | $this->targetTitle |
594 | ); |
595 | |
596 | if ( count( $errors ) ) { |
597 | $key = array_shift( $errors[0] ); |
598 | $message = $this->getContext()->msg( $key ) |
599 | ->params( $errors[0] ) |
600 | ->parse(); |
601 | return Status::newFatal( $message ); |
602 | } |
603 | return Status::newGood(); |
604 | } |
605 | |
606 | /** |
607 | * @param Content $content |
608 | * @param string $summary |
609 | * @return Status |
610 | */ |
611 | protected function runEditFilterMergedContentHook( Content $content, $summary ) { |
612 | $derivativeContext = new DerivativeContext( $this->getContext() ); |
613 | $services = MediaWikiServices::getInstance(); |
614 | $derivativeContext->setConfig( $services->getMainConfig() ); |
615 | $derivativeContext->setTitle( $this->targetTitle ); |
616 | $status = new Status(); |
617 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
618 | if ( !$hookRunner->onEditFilterMergedContent( |
619 | $derivativeContext, |
620 | $content, |
621 | $status, |
622 | $summary, |
623 | $derivativeContext->getUser(), |
624 | false |
625 | ) ) { |
626 | if ( $status->isGood() ) { |
627 | $status->fatal( 'hookaborted' ); |
628 | } |
629 | return $status; |
630 | } |
631 | return $status; |
632 | } |
633 | |
634 | private function getBody() { |
635 | return $this->body; |
636 | } |
637 | |
638 | /** |
639 | * Validate that $content is an instance of Content |
640 | * |
641 | * @param Content|null $content |
642 | * @return Status |
643 | */ |
644 | protected function checkContent( $content ) { |
645 | return $content instanceof Content ? |
646 | Status::newGood() : |
647 | Status::newFatal( |
648 | 'apierror-missingcontent-revid', |
649 | $this->getPageUpdater()->grabParentRevision()->getId() |
650 | ); |
651 | } |
652 | |
653 | private function saveNewQuestion() { |
654 | $question = new QuestionRecord( |
655 | $this->getBody(), |
656 | $this->getSectionHeader(), |
657 | $this->revisionId, |
658 | (int)$this->getPostedOnTimestamp(), |
659 | $this->getResultUrl(), |
660 | $this->getTargetContentModel() |
661 | ); |
662 | QuestionStoreFactory::newFromContextAndStorage( |
663 | $this->getContext(), |
664 | $this->getQuestionStoragePref() |
665 | )->add( $question ); |
666 | } |
667 | |
668 | } |