Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 285 |
|
0.00% |
0 / 29 |
CRAP | |
0.00% |
0 / 1 |
OptInController | |
0.00% |
0 / 285 |
|
0.00% |
0 / 29 |
3422 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
initiateChange | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
enable | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
disable | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
hasFlowBoardArchive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isFlowBoard | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
movePage | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
fatal | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
fromNewlineSeparated | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findLatestArchive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
findNextArchive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
findLatestFlowArchive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
findNextFlowArchive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
createRevision | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
createFlowBoard | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
archiveExistingTalkpage | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
restoreExistingFlowBoard | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
getContent | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getFormattedCurrentTemplate | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
formatTemplate | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
editBoardDescription | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
42 | |||
getFormattedArchiveTemplate | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
removeArchiveTemplateFromWikitextTalkpage | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
removeCurrentTemplateFromWikitext | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
extractTemplatesAboveFirstSection | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
editWikitextContent | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
addCurrentTemplate | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
archiveFlowBoard | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
logBlockErrors | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace Flow\Import; |
4 | |
5 | use DateTime; |
6 | use DateTimeZone; |
7 | use Exception; |
8 | use Flow\Block\AbstractBlock; |
9 | use Flow\Collection\HeaderCollection; |
10 | use Flow\Container; |
11 | use Flow\Content\BoardContent; |
12 | use Flow\Conversion\Utils; |
13 | use Flow\Exception\InvalidDataException; |
14 | use Flow\Notifications\Controller; |
15 | use Flow\OccupationController; |
16 | use Flow\WorkflowLoader; |
17 | use Flow\WorkflowLoaderFactory; |
18 | use FormatJson; |
19 | use IDBAccessObject; |
20 | use MediaWiki\Context\DerivativeContext; |
21 | use MediaWiki\Context\IContextSource; |
22 | use MediaWiki\Context\RequestContext; |
23 | use MediaWiki\Deferred\DeferredUpdates; |
24 | use MediaWiki\Logger\LoggerFactory; |
25 | use MediaWiki\MediaWikiServices; |
26 | use MediaWiki\Revision\RevisionRecord; |
27 | use MediaWiki\Revision\SlotRecord; |
28 | use MediaWiki\Title\Title; |
29 | use MediaWiki\User\User; |
30 | use ParserOptions; |
31 | use Psr\Log\LoggerInterface; |
32 | use WikitextContent; |
33 | |
34 | /** |
35 | * Entry point for enabling Flow on a page. |
36 | */ |
37 | class OptInController { |
38 | public const ENABLE = 'enable'; |
39 | public const DISABLE = 'disable'; |
40 | |
41 | /** |
42 | * @var OccupationController |
43 | */ |
44 | private $occupationController; |
45 | |
46 | /** |
47 | * @var Controller |
48 | */ |
49 | private $notificationController; |
50 | |
51 | /** |
52 | * @var ArchiveNameHelper |
53 | */ |
54 | private $archiveNameHelper; |
55 | |
56 | /** |
57 | * @var LoggerInterface |
58 | */ |
59 | private $logger; |
60 | |
61 | /** |
62 | * @var IContextSource |
63 | */ |
64 | private $context; |
65 | |
66 | /** |
67 | * @var User |
68 | */ |
69 | private $user; |
70 | |
71 | /** |
72 | * @param OccupationController $occupationController |
73 | * @param Controller $notificationController |
74 | * @param ArchiveNameHelper $archiveNameHelper |
75 | * @param LoggerInterface $logger Logger for errors and exceptions |
76 | * @param User $scriptUser User that takes actions, such as creating the board or |
77 | * editing descriptions |
78 | */ |
79 | public function __construct( |
80 | OccupationController $occupationController, |
81 | Controller $notificationController, |
82 | ArchiveNameHelper $archiveNameHelper, |
83 | LoggerInterface $logger, |
84 | User $scriptUser |
85 | ) { |
86 | $this->occupationController = $occupationController; |
87 | $this->notificationController = $notificationController; |
88 | $this->archiveNameHelper = $archiveNameHelper; |
89 | $this->logger = $logger; |
90 | $this->user = $scriptUser; |
91 | $this->context = new DerivativeContext( RequestContext::getMain() ); |
92 | $this->context->setUser( $this->user ); |
93 | } |
94 | |
95 | /** |
96 | * @param string $action Action to take, self::ENABLE or self::DISABLE |
97 | * @param Title $talkpage Title of user's talk page |
98 | * @param User $user User that owns the talk page |
99 | */ |
100 | public function initiateChange( $action, Title $talkpage, User $user ) { |
101 | $outerMethod = __METHOD__; |
102 | $logger = $this->logger; |
103 | |
104 | DeferredUpdates::addCallableUpdate( |
105 | function () use ( $logger, $outerMethod, $action, $talkpage, $user ) { |
106 | try { |
107 | if ( $action === self::ENABLE ) { |
108 | $this->enable( $talkpage, $user ); |
109 | } elseif ( $action === self::DISABLE ) { |
110 | $this->disable( $talkpage ); |
111 | } else { |
112 | $logger->error( $outerMethod . ': unrecognized action: ' . $action ); |
113 | } |
114 | } catch ( Exception $exception ) { |
115 | $logger->error( |
116 | $outerMethod . ' failed to {action} Flow on \'{talkpage}\' for user \'{user}\'. {message} {trace}', |
117 | [ |
118 | 'action' => $action, |
119 | 'talkpage' => $talkpage->getPrefixedText(), |
120 | 'user' => $user->getName(), |
121 | 'message' => $exception->getMessage(), |
122 | 'exception' => $exception, |
123 | ] |
124 | ); |
125 | |
126 | // Rollback both Flow and Core DBs. |
127 | MediaWikiServices::getInstance()->getDBLoadBalancerFactory() |
128 | ->rollbackPrimaryChanges( $outerMethod ); |
129 | } |
130 | }, |
131 | DeferredUpdates::POSTSEND |
132 | ); |
133 | } |
134 | |
135 | /** |
136 | * @param Title $title |
137 | * @param User $user |
138 | */ |
139 | public function enable( Title $title, User $user ) { |
140 | if ( $this->isFlowBoard( $title ) ) { |
141 | // already a Flow board |
142 | return; |
143 | } |
144 | |
145 | // archive existing wikitext talk page |
146 | $currentTemplate = null; |
147 | $templatesFromTalkpage = null; |
148 | if ( $title->exists( IDBAccessObject::READ_LATEST ) ) { |
149 | $templatesFromTalkpage = $this->extractTemplatesAboveFirstSection( $title ); |
150 | $wikitextTalkpageArchiveTitle = $this->archiveExistingTalkpage( $title ); |
151 | $currentTemplate = $this->getFormattedCurrentTemplate( $wikitextTalkpageArchiveTitle ); |
152 | } |
153 | |
154 | // create or restore flow board |
155 | $archivedFlowPage = $this->findLatestFlowArchive( $title ); |
156 | if ( $archivedFlowPage ) { |
157 | $this->restoreExistingFlowBoard( $archivedFlowPage, $title, $currentTemplate ); |
158 | } else { |
159 | $this->createFlowBoard( $title, $templatesFromTalkpage . "\n\n" . $currentTemplate ); |
160 | $this->notificationController->notifyFlowEnabledOnTalkpage( $user ); |
161 | } |
162 | } |
163 | |
164 | /** |
165 | * @param Title $title |
166 | */ |
167 | public function disable( Title $title ) { |
168 | if ( !$this->isFlowBoard( $title ) ) { |
169 | return; |
170 | } |
171 | |
172 | // archive the flow board |
173 | $flowArchiveTitle = $this->archiveFlowBoard( $title ); |
174 | |
175 | // restore the original wikitext talk page |
176 | $archivedTalkpage = $this->findLatestArchive( $title ); |
177 | if ( $archivedTalkpage ) { |
178 | $this->removeArchiveTemplateFromWikitextTalkpage( $archivedTalkpage ); |
179 | $this->addCurrentTemplate( $archivedTalkpage, $flowArchiveTitle ); |
180 | $restoreReason = wfMessage( 'flow-optin-restore-wikitext' )->inContentLanguage()->text(); |
181 | $this->movePage( $archivedTalkpage, $title, $restoreReason ); |
182 | } |
183 | } |
184 | |
185 | /** |
186 | * Check whether the current user has a flow board archived already. |
187 | * |
188 | * @param User $user |
189 | * @return bool Flow board archive exists |
190 | */ |
191 | public function hasFlowBoardArchive( User $user ) { |
192 | return $this->findLatestFlowArchive( $user->getTalkPage() ) !== false; |
193 | } |
194 | |
195 | /** |
196 | * @param Title $title |
197 | * @return bool |
198 | */ |
199 | private function isFlowBoard( Title $title ) { |
200 | return $title->getContentModel( IDBAccessObject::READ_LATEST ) === CONTENT_MODEL_FLOW_BOARD; |
201 | } |
202 | |
203 | /** |
204 | * @param Title $from |
205 | * @param Title $to |
206 | * @param string $reason |
207 | */ |
208 | private function movePage( Title $from, Title $to, $reason = '' ) { |
209 | $this->occupationController->forceAllowCreation( $to ); |
210 | |
211 | $mp = MediaWikiServices::getInstance() |
212 | ->getMovePageFactory() |
213 | ->newMovePage( $from, $to ); |
214 | $mp->move( $this->user, $reason, false ); |
215 | |
216 | /* |
217 | * Article IDs are cached inside title objects. Since we'll be |
218 | * reusing these objects, we have to make sure they reflect the |
219 | * correct IDs. |
220 | * |
221 | * We could just IDBAccessObject::READ_LATEST everywhere, but that would |
222 | * result in a lot of unneeded calls to primary database. |
223 | * |
224 | * If these IDs are wrong, we could end up associating workflows |
225 | * with an incorrect page (that was just moved) |
226 | * |
227 | * Anyway, the page has just been moved without redirect, so that |
228 | * page is no longer valid. |
229 | */ |
230 | $from->resetArticleID( 0 ); |
231 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
232 | $linkCache->addBadLinkObj( $from ); |
233 | |
234 | /* |
235 | * Force id cached inside $title to be updated, as well as info |
236 | * inside LinkCache. |
237 | */ |
238 | $to->getArticleID( IDBAccessObject::READ_LATEST ); |
239 | } |
240 | |
241 | /** |
242 | * @param string $msgKey |
243 | * @param mixed $args |
244 | * @throws ImportException |
245 | * @return never |
246 | */ |
247 | private function fatal( $msgKey, $args = [] ) { |
248 | throw new ImportException( wfMessage( $msgKey, $args )->inContentLanguage()->text() ); |
249 | } |
250 | |
251 | /** |
252 | * @param string $str |
253 | * @return string[] |
254 | */ |
255 | private function fromNewlineSeparated( $str ) { |
256 | return explode( "\n", $str ); |
257 | } |
258 | |
259 | /** |
260 | * @param Title $title |
261 | * @return Title|false |
262 | */ |
263 | private function findLatestArchive( Title $title ) { |
264 | $archiveFormats = $this->fromNewlineSeparated( |
265 | wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() ); |
266 | return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats ); |
267 | } |
268 | |
269 | /** |
270 | * @param Title $title |
271 | * @return Title |
272 | * @throws ImportException |
273 | */ |
274 | private function findNextArchive( Title $title ) { |
275 | $archiveFormats = $this->fromNewlineSeparated( |
276 | wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() ); |
277 | return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats ); |
278 | } |
279 | |
280 | /** |
281 | * @param Title $title |
282 | * @return Title|false |
283 | */ |
284 | private function findLatestFlowArchive( Title $title ) { |
285 | $archiveFormats = $this->fromNewlineSeparated( |
286 | wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() ); |
287 | return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats ); |
288 | } |
289 | |
290 | /** |
291 | * @param Title $title |
292 | * @return Title |
293 | * @throws ImportException |
294 | */ |
295 | private function findNextFlowArchive( Title $title ) { |
296 | $archiveFormats = $this->fromNewlineSeparated( |
297 | wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() ); |
298 | return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats ); |
299 | } |
300 | |
301 | /** |
302 | * @param Title $title |
303 | * @param string $contentText |
304 | * @param string $summary |
305 | * @throws ImportException |
306 | * @param-taint $contentText escapes_escaped |
307 | */ |
308 | private function createRevision( Title $title, $contentText, $summary ) { |
309 | $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
310 | $newContent = new WikitextContent( $contentText ); |
311 | $status = $page->doUserEditContent( |
312 | $newContent, |
313 | $this->user, |
314 | $summary, |
315 | EDIT_FORCE_BOT | EDIT_SUPPRESS_RC |
316 | ); |
317 | |
318 | if ( !$status->isGood() ) { |
319 | throw new ImportException( "Failed creating revision at {$title}" ); |
320 | } |
321 | } |
322 | |
323 | /** |
324 | * @param Title $title |
325 | * @param string $boardDescription |
326 | * @throws ImportException |
327 | * @throws \Flow\Exception\CrossWikiException |
328 | * @throws \Flow\Exception\InvalidInputException |
329 | */ |
330 | private function createFlowBoard( Title $title, $boardDescription ) { |
331 | /** @var WorkflowLoaderFactory $loaderFactory */ |
332 | $loaderFactory = Container::get( 'factory.loader.workflow' ); |
333 | $page = $title->getPrefixedText(); |
334 | |
335 | $creationStatus = $this->occupationController->safeAllowCreation( $title, $this->user, false ); |
336 | if ( !$creationStatus->isGood() ) { |
337 | $this->fatal( 'flow-special-enableflow-board-creation-not-allowed', $page ); |
338 | } |
339 | |
340 | $loader = $loaderFactory->createWorkflowLoader( $title ); |
341 | $blocks = $loader->getBlocks(); |
342 | $this->logBlockErrors( $blocks ); |
343 | |
344 | if ( !$boardDescription ) { |
345 | $boardDescription = ' '; |
346 | } |
347 | |
348 | $action = 'edit-header'; |
349 | $params = [ |
350 | 'header' => [ |
351 | 'content' => $boardDescription, |
352 | 'format' => 'wikitext', |
353 | ], |
354 | ]; |
355 | |
356 | $blocksToCommit = $loader->handleSubmit( |
357 | $this->context, |
358 | $action, |
359 | $params |
360 | ); |
361 | |
362 | foreach ( $blocks as $block ) { |
363 | if ( $block->hasErrors() ) { |
364 | $errors = $block->getErrors(); |
365 | |
366 | foreach ( $errors as $errorKey ) { |
367 | $this->fatal( $block->getErrorMessage( $errorKey ) ); |
368 | } |
369 | } |
370 | } |
371 | |
372 | $loader->commit( $blocksToCommit ); |
373 | } |
374 | |
375 | /** |
376 | * @param Title $title |
377 | * @return Title |
378 | */ |
379 | private function archiveExistingTalkpage( Title $title ) { |
380 | $archiveTitle = $this->findNextArchive( $title ); |
381 | $archiveReason = wfMessage( 'flow-optin-archive-wikitext' )->inContentLanguage()->text(); |
382 | $this->movePage( $title, $archiveTitle, $archiveReason ); |
383 | |
384 | $content = $this->getContent( $archiveTitle ); |
385 | $content = $this->removeCurrentTemplateFromWikitext( $content, $archiveTitle ); |
386 | $content = $this->getFormattedArchiveTemplate( $title ) . "\n\n" . $content; |
387 | |
388 | $addTemplateReason = wfMessage( 'flow-beta-feature-add-archive-template-edit-summary' )->inContentLanguage()->plain(); |
389 | $this->createRevision( |
390 | $archiveTitle, |
391 | $content, |
392 | $addTemplateReason |
393 | ); |
394 | |
395 | return $archiveTitle; |
396 | } |
397 | |
398 | /** |
399 | * @param Title $archivedFlowPage |
400 | * @param Title $title |
401 | * @param string|null $currentTemplate |
402 | */ |
403 | private function restoreExistingFlowBoard( Title $archivedFlowPage, Title $title, $currentTemplate = null ) { |
404 | $this->editBoardDescription( |
405 | $archivedFlowPage, |
406 | static function ( $content ) use ( $currentTemplate, $archivedFlowPage ) { |
407 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
408 | $content = TemplateHelper::removeFromHtml( $content, $templateName ); |
409 | if ( $currentTemplate ) { |
410 | $content = Utils::convert( 'wikitext', 'html', $currentTemplate, $archivedFlowPage ) . "<br/><br/>" . $content; |
411 | } |
412 | return $content; |
413 | }, |
414 | 'html' |
415 | ); |
416 | |
417 | $restoreReason = wfMessage( 'flow-optin-restore-flow-board' )->inContentLanguage()->text(); |
418 | $this->movePage( $archivedFlowPage, $title, $restoreReason ); |
419 | } |
420 | |
421 | /** |
422 | * @param Title $title |
423 | * @return string |
424 | */ |
425 | private function getContent( Title $title ) { |
426 | $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
427 | $page->loadPageData( IDBAccessObject::READ_LATEST ); |
428 | $revision = $page->getRevisionRecord(); |
429 | if ( $revision ) { |
430 | $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ); |
431 | if ( $content instanceof WikitextContent ) { |
432 | return $content->getText(); |
433 | } |
434 | } |
435 | |
436 | return ''; |
437 | } |
438 | |
439 | /** |
440 | * @param Title $archiveTitle |
441 | * @return string |
442 | */ |
443 | private function getFormattedCurrentTemplate( Title $archiveTitle ) { |
444 | $now = new DateTime( "now", new DateTimeZone( "GMT" ) ); |
445 | $arguments = [ |
446 | 'archive' => $archiveTitle->getPrefixedText(), |
447 | 'date' => $now->format( 'Y-m-d' ), |
448 | ]; |
449 | $template = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
450 | return $this->formatTemplate( $template, $arguments ); |
451 | } |
452 | |
453 | /** |
454 | * @param string $name |
455 | * @param array $args |
456 | * @return string |
457 | */ |
458 | private function formatTemplate( $name, array $args ) { |
459 | $arguments = implode( '|', |
460 | array_map( |
461 | static function ( $key, $value ) { |
462 | return "$key=$value"; |
463 | }, |
464 | array_keys( $args ), |
465 | array_values( $args ) ) |
466 | ); |
467 | return "{{{$name}|$arguments}}"; |
468 | } |
469 | |
470 | /** |
471 | * @param Title $title |
472 | * @param callable $newDescriptionCallback |
473 | * @param string $format |
474 | * @throws ImportException |
475 | * @throws InvalidDataException |
476 | */ |
477 | private function editBoardDescription( Title $title, callable $newDescriptionCallback, $format = 'html' ) { |
478 | /* |
479 | * We could use WorkflowLoaderFactory::createWorkflowLoader |
480 | * to get to the workflow ID, but that uses WikiPageFactory::newFromTitle |
481 | * to build the wikipage & get the content. For most requests, |
482 | * that'll be better (it reads from replicas), but we really |
483 | * need to read from primary database here. |
484 | * We'll need WorkflowLoader further down anyway, but we'll |
485 | * then have the correct workflow ID to initialize it with! |
486 | * |
487 | * $title->getLatestRevId() should be fine, it'll be read from |
488 | * LinkCache, which has been updated. |
489 | * RevisionLookup::getRevisionById will try replica first. |
490 | * If it can't find the id, it'll try to find it on primary database. |
491 | */ |
492 | $revId = $title->getLatestRevID(); |
493 | $revRecord = MediaWikiServices::getInstance() |
494 | ->getRevisionLookup() |
495 | ->getRevisionById( $revId ); |
496 | $content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN ) : null; |
497 | if ( !$content instanceof BoardContent ) { |
498 | throw new InvalidDataException( |
499 | 'Could not find board page for ' . $title->getPrefixedDBkey() . ' (id: ' . $title->getArticleID() . ').' . |
500 | 'Found content: ' . var_export( $content, true ) |
501 | ); |
502 | } |
503 | $workflowId = $content->getWorkflowId(); |
504 | |
505 | $collection = HeaderCollection::newFromId( $workflowId ); |
506 | $revision = $collection->getLastRevision(); |
507 | |
508 | /* |
509 | * We could just do $revision->getContent( $format ), but that |
510 | * may need to find $title in order to convert. |
511 | * We already know $title (and don't want to risk it being used |
512 | * in a way it stores lagging replica data), so let's just |
513 | * manually convert the content. |
514 | */ |
515 | $content = $revision->getContentRaw(); |
516 | $content = Utils::convert( $revision->getContentFormat(), $format, $content, $title ); |
517 | |
518 | $newDescription = $newDescriptionCallback( $content ); |
519 | |
520 | $action = 'edit-header'; |
521 | $params = [ |
522 | 'header' => [ |
523 | 'content' => $newDescription, |
524 | 'format' => $format, |
525 | 'prev_revision' => $revision->getRevisionId()->getAlphadecimal() |
526 | ], |
527 | ]; |
528 | |
529 | /** @var WorkflowLoaderFactory $factory */ |
530 | $factory = Container::get( 'factory.loader.workflow' ); |
531 | |
532 | /** @var WorkflowLoader $loader */ |
533 | $loader = $factory->createWorkflowLoader( $title, $workflowId ); |
534 | |
535 | $blocks = $loader->getBlocks(); |
536 | $this->logBlockErrors( $blocks ); |
537 | |
538 | $blocksToCommit = $loader->handleSubmit( |
539 | $this->context, |
540 | $action, |
541 | $params |
542 | ); |
543 | |
544 | foreach ( $blocks as $block ) { |
545 | if ( $block->hasErrors() ) { |
546 | $errors = $block->getErrors(); |
547 | |
548 | foreach ( $errors as $errorKey ) { |
549 | $this->fatal( $block->getErrorMessage( $errorKey ) ); |
550 | } |
551 | } |
552 | } |
553 | |
554 | $loader->commit( $blocksToCommit ); |
555 | } |
556 | |
557 | /** |
558 | * @param Title $current |
559 | * @return string |
560 | */ |
561 | private function getFormattedArchiveTemplate( Title $current ) { |
562 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
563 | $now = new DateTime( "now", new DateTimeZone( "GMT" ) ); |
564 | return $this->formatTemplate( $templateName, [ |
565 | 'from' => $current->getPrefixedText(), |
566 | 'date' => $now->format( 'Y-m-d' ), |
567 | ] ); |
568 | } |
569 | |
570 | /** |
571 | * @param Title $title |
572 | * @throws ImportException |
573 | */ |
574 | private function removeArchiveTemplateFromWikitextTalkpage( Title $title ) { |
575 | $wtContent = $this->getContent( $title ); |
576 | if ( !$wtContent ) { |
577 | return; |
578 | } |
579 | |
580 | $content = Utils::convert( 'wikitext', 'html', $wtContent, $title ); |
581 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
582 | |
583 | $newContent = TemplateHelper::removeFromHtml( $content, $templateName ); |
584 | |
585 | $this->createRevision( |
586 | $title, |
587 | Utils::convert( 'html', 'wikitext', $newContent, $title ), |
588 | wfMessage( 'flow-beta-feature-remove-archive-template-edit-summary' )->inContentLanguage()->plain() ); |
589 | } |
590 | |
591 | /** |
592 | * @param string $wikitextContent |
593 | * @param Title $title |
594 | * @return string |
595 | */ |
596 | private function removeCurrentTemplateFromWikitext( $wikitextContent, Title $title ) { |
597 | $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
598 | $contentAsHtml = Utils::convert( 'wikitext', 'html', $wikitextContent, $title ); |
599 | $contentWithoutTemplate = TemplateHelper::removeFromHtml( $contentAsHtml, $templateName ); |
600 | return Utils::convert( 'html', 'wikitext', $contentWithoutTemplate, $title ); |
601 | } |
602 | |
603 | /** |
604 | * @param Title $title |
605 | * @return string |
606 | */ |
607 | private function extractTemplatesAboveFirstSection( Title $title ) { |
608 | $content = $this->getContent( $title ); |
609 | if ( !$content ) { |
610 | return ''; |
611 | } |
612 | |
613 | $parser = MediaWikiServices::getInstance()->getParserFactory()->create(); |
614 | $output = $parser->parse( $content, $title, new ParserOptions( $this->user ) ); |
615 | $sections = $output->getSections(); |
616 | if ( $sections ) { |
617 | # T319141: `byteoffset` is actually a *codepoint* offset. |
618 | $content = mb_substr( $content, 0, $sections[0]['byteoffset'] ); |
619 | } |
620 | return TemplateHelper::extractTemplates( $content, $title ); |
621 | } |
622 | |
623 | /** |
624 | * @param Title $title |
625 | * @param string $reason |
626 | * @param callable $newDescriptionCallback |
627 | * @param string $format |
628 | * @throws ImportException |
629 | * @throws InvalidDataException |
630 | */ |
631 | private function editWikitextContent( Title $title, $reason, callable $newDescriptionCallback, $format = 'html' ) { |
632 | $content = Utils::convert( 'wikitext', $format, $this->getContent( $title ), $title ); |
633 | $newContent = $newDescriptionCallback( $content ); |
634 | $this->createRevision( |
635 | $title, |
636 | Utils::convert( $format, 'wikitext', $newContent, $title ), |
637 | $reason |
638 | ); |
639 | } |
640 | |
641 | /** |
642 | * Add the "current" template to the page considered the current talkpage |
643 | * and link to the archived talkpage. |
644 | * |
645 | * @param Title $currentTalkpageTitle |
646 | * @param Title $archivedTalkpageTitle |
647 | */ |
648 | private function addCurrentTemplate( Title $currentTalkpageTitle, Title $archivedTalkpageTitle ) { |
649 | $template = $this->getFormattedCurrentTemplate( $archivedTalkpageTitle ); |
650 | $this->editWikitextContent( |
651 | $currentTalkpageTitle, |
652 | wfMessage( 'flow-beta-feature-add-current-template-edit-summary' )->inContentLanguage()->plain(), |
653 | static function ( $content ) use ( $template ) { |
654 | return $template . "\n\n" . $content; |
655 | }, |
656 | 'wikitext' |
657 | ); |
658 | } |
659 | |
660 | /** |
661 | * @param Title $title |
662 | * @return Title |
663 | * @throws InvalidDataException |
664 | */ |
665 | private function archiveFlowBoard( Title $title ) { |
666 | $flowArchiveTitle = $this->findNextFlowArchive( $title ); |
667 | $archiveReason = wfMessage( 'flow-optin-archive-flow-board' )->inContentLanguage()->text(); |
668 | $this->movePage( $title, $flowArchiveTitle, $archiveReason ); |
669 | |
670 | $template = $this->getFormattedArchiveTemplate( $title ); |
671 | $template = Utils::convert( 'wikitext', 'html', $template, $title ); |
672 | |
673 | $this->editBoardDescription( |
674 | $flowArchiveTitle, |
675 | static function ( $content ) use ( $template ) { |
676 | $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
677 | $content = TemplateHelper::removeFromHtml( $content, $templateName ); |
678 | return $template . "<br/><br/>" . $content; |
679 | }, |
680 | 'html' ); |
681 | |
682 | return $flowArchiveTitle; |
683 | } |
684 | |
685 | /** |
686 | * @param array $blocks |
687 | */ |
688 | private function logBlockErrors( array $blocks ) { |
689 | $errors = []; |
690 | /** @var AbstractBlock $block */ |
691 | foreach ( $blocks as $block ) { |
692 | if ( $block->hasErrors() ) { |
693 | $blockErrors = $block->getErrors(); |
694 | foreach ( $blockErrors as $blockErrorType ) { |
695 | $errors[ $block->getName() ] = [ |
696 | 'type' => $blockErrorType, |
697 | 'message' => $block->getErrorMessage( $blockErrorType ), |
698 | 'extra' => $block->getErrorExtra( $blockErrorType ) |
699 | ]; |
700 | } |
701 | } |
702 | } |
703 | if ( $errors ) { |
704 | LoggerFactory::getInstance( 'Flow' )->error( |
705 | 'Found {count} block errors for user {user_id}', |
706 | [ |
707 | 'count' => count( $errors ), |
708 | 'user_id' => RequestContext::getMain()->getUser()->getId(), |
709 | 'errors' => FormatJson::encode( $errors ) |
710 | ] |
711 | ); |
712 | } |
713 | } |
714 | } |