Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 293 |
|
0.00% |
0 / 29 |
CRAP | |
0.00% |
0 / 1 |
OptInController | |
0.00% |
0 / 293 |
|
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 / 18 |
|
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 MediaWiki\Content\WikitextContent; |
19 | use MediaWiki\Context\DerivativeContext; |
20 | use MediaWiki\Context\IContextSource; |
21 | use MediaWiki\Context\RequestContext; |
22 | use MediaWiki\Deferred\DeferredUpdates; |
23 | use MediaWiki\Json\FormatJson; |
24 | use MediaWiki\Logger\LoggerFactory; |
25 | use MediaWiki\MediaWikiServices; |
26 | use MediaWiki\Parser\ParserOptions; |
27 | use MediaWiki\Revision\RevisionRecord; |
28 | use MediaWiki\Revision\SlotRecord; |
29 | use MediaWiki\Title\Title; |
30 | use MediaWiki\User\User; |
31 | use Psr\Log\LoggerInterface; |
32 | use Wikimedia\Rdbms\IDBAccessObject; |
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 | $statusFormatter = MediaWikiServices::getInstance()->getFormatterFactory()->getStatusFormatter( |
320 | $this->context |
321 | ); |
322 | $this->logger->error( 'Failed creating revision at {title} because {status}', [ |
323 | 'title' => $title->getPrefixedText(), |
324 | 'status' => $statusFormatter->getWikiText( $status, [ 'lang' => 'en' ] ), |
325 | 'exception' => new \RuntimeException(), |
326 | ] ); |
327 | throw new ImportException( "Failed creating revision at {$title}" ); |
328 | } |
329 | } |
330 | |
331 | /** |
332 | * @param Title $title |
333 | * @param string $boardDescription |
334 | * @throws ImportException |
335 | * @throws \Flow\Exception\CrossWikiException |
336 | * @throws \Flow\Exception\InvalidInputException |
337 | */ |
338 | private function createFlowBoard( Title $title, $boardDescription ) { |
339 | /** @var WorkflowLoaderFactory $loaderFactory */ |
340 | $loaderFactory = Container::get( 'factory.loader.workflow' ); |
341 | $page = $title->getPrefixedText(); |
342 | |
343 | $creationStatus = $this->occupationController->safeAllowCreation( $title, $this->user, false ); |
344 | if ( !$creationStatus->isGood() ) { |
345 | $this->fatal( 'flow-special-enableflow-board-creation-not-allowed', $page ); |
346 | } |
347 | |
348 | $loader = $loaderFactory->createWorkflowLoader( $title ); |
349 | $blocks = $loader->getBlocks(); |
350 | $this->logBlockErrors( $blocks ); |
351 | |
352 | if ( !$boardDescription ) { |
353 | $boardDescription = ' '; |
354 | } |
355 | |
356 | $action = 'edit-header'; |
357 | $params = [ |
358 | 'header' => [ |
359 | 'content' => $boardDescription, |
360 | 'format' => 'wikitext', |
361 | ], |
362 | ]; |
363 | |
364 | $blocksToCommit = $loader->handleSubmit( |
365 | $this->context, |
366 | $action, |
367 | $params |
368 | ); |
369 | |
370 | foreach ( $blocks as $block ) { |
371 | if ( $block->hasErrors() ) { |
372 | $errors = $block->getErrors(); |
373 | |
374 | foreach ( $errors as $errorKey ) { |
375 | $this->fatal( $block->getErrorMessage( $errorKey ) ); |
376 | } |
377 | } |
378 | } |
379 | |
380 | $loader->commit( $blocksToCommit ); |
381 | } |
382 | |
383 | /** |
384 | * @param Title $title |
385 | * @return Title |
386 | */ |
387 | private function archiveExistingTalkpage( Title $title ) { |
388 | $archiveTitle = $this->findNextArchive( $title ); |
389 | $archiveReason = wfMessage( 'flow-optin-archive-wikitext' )->inContentLanguage()->text(); |
390 | $this->movePage( $title, $archiveTitle, $archiveReason ); |
391 | |
392 | $content = $this->getContent( $archiveTitle ); |
393 | $content = $this->removeCurrentTemplateFromWikitext( $content, $archiveTitle ); |
394 | $content = $this->getFormattedArchiveTemplate( $title ) . "\n\n" . $content; |
395 | |
396 | $addTemplateReason = wfMessage( 'flow-beta-feature-add-archive-template-edit-summary' )->inContentLanguage()->plain(); |
397 | $this->createRevision( |
398 | $archiveTitle, |
399 | $content, |
400 | $addTemplateReason |
401 | ); |
402 | |
403 | return $archiveTitle; |
404 | } |
405 | |
406 | /** |
407 | * @param Title $archivedFlowPage |
408 | * @param Title $title |
409 | * @param string|null $currentTemplate |
410 | */ |
411 | private function restoreExistingFlowBoard( Title $archivedFlowPage, Title $title, $currentTemplate = null ) { |
412 | $this->editBoardDescription( |
413 | $archivedFlowPage, |
414 | static function ( $content ) use ( $currentTemplate, $archivedFlowPage ) { |
415 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
416 | $content = TemplateHelper::removeFromHtml( $content, $templateName ); |
417 | if ( $currentTemplate ) { |
418 | $content = Utils::convert( 'wikitext', 'html', $currentTemplate, $archivedFlowPage ) . "<br/><br/>" . $content; |
419 | } |
420 | return $content; |
421 | }, |
422 | 'html' |
423 | ); |
424 | |
425 | $restoreReason = wfMessage( 'flow-optin-restore-flow-board' )->inContentLanguage()->text(); |
426 | $this->movePage( $archivedFlowPage, $title, $restoreReason ); |
427 | } |
428 | |
429 | /** |
430 | * @param Title $title |
431 | * @return string |
432 | */ |
433 | private function getContent( Title $title ) { |
434 | $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
435 | $page->loadPageData( IDBAccessObject::READ_LATEST ); |
436 | $revision = $page->getRevisionRecord(); |
437 | if ( $revision ) { |
438 | $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ); |
439 | if ( $content instanceof WikitextContent ) { |
440 | return $content->getText(); |
441 | } |
442 | } |
443 | |
444 | return ''; |
445 | } |
446 | |
447 | /** |
448 | * @param Title $archiveTitle |
449 | * @return string |
450 | */ |
451 | private function getFormattedCurrentTemplate( Title $archiveTitle ) { |
452 | $now = new DateTime( "now", new DateTimeZone( "GMT" ) ); |
453 | $arguments = [ |
454 | 'archive' => $archiveTitle->getPrefixedText(), |
455 | 'date' => $now->format( 'Y-m-d' ), |
456 | ]; |
457 | $template = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
458 | return $this->formatTemplate( $template, $arguments ); |
459 | } |
460 | |
461 | /** |
462 | * @param string $name |
463 | * @param array $args |
464 | * @return string |
465 | */ |
466 | private function formatTemplate( $name, array $args ) { |
467 | $arguments = implode( '|', |
468 | array_map( |
469 | static function ( $key, $value ) { |
470 | return "$key=$value"; |
471 | }, |
472 | array_keys( $args ), |
473 | array_values( $args ) ) |
474 | ); |
475 | return "{{{$name}|$arguments}}"; |
476 | } |
477 | |
478 | /** |
479 | * @param Title $title |
480 | * @param callable $newDescriptionCallback |
481 | * @param string $format |
482 | * @throws ImportException |
483 | * @throws InvalidDataException |
484 | */ |
485 | private function editBoardDescription( Title $title, callable $newDescriptionCallback, $format = 'html' ) { |
486 | /* |
487 | * We could use WorkflowLoaderFactory::createWorkflowLoader |
488 | * to get to the workflow ID, but that uses WikiPageFactory::newFromTitle |
489 | * to build the wikipage & get the content. For most requests, |
490 | * that'll be better (it reads from replicas), but we really |
491 | * need to read from primary database here. |
492 | * We'll need WorkflowLoader further down anyway, but we'll |
493 | * then have the correct workflow ID to initialize it with! |
494 | * |
495 | * $title->getLatestRevId() should be fine, it'll be read from |
496 | * LinkCache, which has been updated. |
497 | * RevisionLookup::getRevisionById will try replica first. |
498 | * If it can't find the id, it'll try to find it on primary database. |
499 | */ |
500 | $revId = $title->getLatestRevID(); |
501 | $revRecord = MediaWikiServices::getInstance() |
502 | ->getRevisionLookup() |
503 | ->getRevisionById( $revId ); |
504 | $content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN ) : null; |
505 | if ( !$content instanceof BoardContent ) { |
506 | throw new InvalidDataException( |
507 | 'Could not find board page for ' . $title->getPrefixedDBkey() . ' (id: ' . $title->getArticleID() . ').' . |
508 | 'Found content: ' . var_export( $content, true ) |
509 | ); |
510 | } |
511 | $workflowId = $content->getWorkflowId(); |
512 | |
513 | $collection = HeaderCollection::newFromId( $workflowId ); |
514 | $revision = $collection->getLastRevision(); |
515 | |
516 | /* |
517 | * We could just do $revision->getContent( $format ), but that |
518 | * may need to find $title in order to convert. |
519 | * We already know $title (and don't want to risk it being used |
520 | * in a way it stores lagging replica data), so let's just |
521 | * manually convert the content. |
522 | */ |
523 | $content = $revision->getContentRaw(); |
524 | $content = Utils::convert( $revision->getContentFormat(), $format, $content, $title ); |
525 | |
526 | $newDescription = $newDescriptionCallback( $content ); |
527 | |
528 | $action = 'edit-header'; |
529 | $params = [ |
530 | 'header' => [ |
531 | 'content' => $newDescription, |
532 | 'format' => $format, |
533 | 'prev_revision' => $revision->getRevisionId()->getAlphadecimal() |
534 | ], |
535 | ]; |
536 | |
537 | /** @var WorkflowLoaderFactory $factory */ |
538 | $factory = Container::get( 'factory.loader.workflow' ); |
539 | |
540 | /** @var WorkflowLoader $loader */ |
541 | $loader = $factory->createWorkflowLoader( $title, $workflowId ); |
542 | |
543 | $blocks = $loader->getBlocks(); |
544 | $this->logBlockErrors( $blocks ); |
545 | |
546 | $blocksToCommit = $loader->handleSubmit( |
547 | $this->context, |
548 | $action, |
549 | $params |
550 | ); |
551 | |
552 | foreach ( $blocks as $block ) { |
553 | if ( $block->hasErrors() ) { |
554 | $errors = $block->getErrors(); |
555 | |
556 | foreach ( $errors as $errorKey ) { |
557 | $this->fatal( $block->getErrorMessage( $errorKey ) ); |
558 | } |
559 | } |
560 | } |
561 | |
562 | $loader->commit( $blocksToCommit ); |
563 | } |
564 | |
565 | /** |
566 | * @param Title $current |
567 | * @return string |
568 | */ |
569 | private function getFormattedArchiveTemplate( Title $current ) { |
570 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
571 | $now = new DateTime( "now", new DateTimeZone( "GMT" ) ); |
572 | return $this->formatTemplate( $templateName, [ |
573 | 'from' => $current->getPrefixedText(), |
574 | 'date' => $now->format( 'Y-m-d' ), |
575 | ] ); |
576 | } |
577 | |
578 | /** |
579 | * @param Title $title |
580 | * @throws ImportException |
581 | */ |
582 | private function removeArchiveTemplateFromWikitextTalkpage( Title $title ) { |
583 | $wtContent = $this->getContent( $title ); |
584 | if ( !$wtContent ) { |
585 | return; |
586 | } |
587 | |
588 | $content = Utils::convert( 'wikitext', 'html', $wtContent, $title ); |
589 | $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain(); |
590 | |
591 | $newContent = TemplateHelper::removeFromHtml( $content, $templateName ); |
592 | |
593 | $this->createRevision( |
594 | $title, |
595 | Utils::convert( 'html', 'wikitext', $newContent, $title ), |
596 | wfMessage( 'flow-beta-feature-remove-archive-template-edit-summary' )->inContentLanguage()->plain() ); |
597 | } |
598 | |
599 | /** |
600 | * @param string $wikitextContent |
601 | * @param Title $title |
602 | * @return string |
603 | */ |
604 | private function removeCurrentTemplateFromWikitext( $wikitextContent, Title $title ) { |
605 | $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
606 | $contentAsHtml = Utils::convert( 'wikitext', 'html', $wikitextContent, $title ); |
607 | $contentWithoutTemplate = TemplateHelper::removeFromHtml( $contentAsHtml, $templateName ); |
608 | return Utils::convert( 'html', 'wikitext', $contentWithoutTemplate, $title ); |
609 | } |
610 | |
611 | /** |
612 | * @param Title $title |
613 | * @return string |
614 | */ |
615 | private function extractTemplatesAboveFirstSection( Title $title ) { |
616 | $content = $this->getContent( $title ); |
617 | if ( !$content ) { |
618 | return ''; |
619 | } |
620 | |
621 | $parser = MediaWikiServices::getInstance()->getParserFactory()->create(); |
622 | $output = $parser->parse( $content, $title, new ParserOptions( $this->user ) ); |
623 | $sections = $output->getSections(); |
624 | if ( $sections ) { |
625 | # T319141: `byteoffset` is actually a *codepoint* offset. |
626 | $content = mb_substr( $content, 0, $sections[0]['byteoffset'] ); |
627 | } |
628 | return TemplateHelper::extractTemplates( $content, $title ); |
629 | } |
630 | |
631 | /** |
632 | * @param Title $title |
633 | * @param string $reason |
634 | * @param callable $newDescriptionCallback |
635 | * @param string $format |
636 | * @throws ImportException |
637 | * @throws InvalidDataException |
638 | */ |
639 | private function editWikitextContent( Title $title, $reason, callable $newDescriptionCallback, $format = 'html' ) { |
640 | $content = Utils::convert( 'wikitext', $format, $this->getContent( $title ), $title ); |
641 | $newContent = $newDescriptionCallback( $content ); |
642 | $this->createRevision( |
643 | $title, |
644 | Utils::convert( $format, 'wikitext', $newContent, $title ), |
645 | $reason |
646 | ); |
647 | } |
648 | |
649 | /** |
650 | * Add the "current" template to the page considered the current talkpage |
651 | * and link to the archived talkpage. |
652 | * |
653 | * @param Title $currentTalkpageTitle |
654 | * @param Title $archivedTalkpageTitle |
655 | */ |
656 | private function addCurrentTemplate( Title $currentTalkpageTitle, Title $archivedTalkpageTitle ) { |
657 | $template = $this->getFormattedCurrentTemplate( $archivedTalkpageTitle ); |
658 | $this->editWikitextContent( |
659 | $currentTalkpageTitle, |
660 | wfMessage( 'flow-beta-feature-add-current-template-edit-summary' )->inContentLanguage()->plain(), |
661 | static function ( $content ) use ( $template ) { |
662 | return $template . "\n\n" . $content; |
663 | }, |
664 | 'wikitext' |
665 | ); |
666 | } |
667 | |
668 | /** |
669 | * @param Title $title |
670 | * @return Title |
671 | * @throws InvalidDataException |
672 | */ |
673 | private function archiveFlowBoard( Title $title ) { |
674 | $flowArchiveTitle = $this->findNextFlowArchive( $title ); |
675 | $archiveReason = wfMessage( 'flow-optin-archive-flow-board' )->inContentLanguage()->text(); |
676 | $this->movePage( $title, $flowArchiveTitle, $archiveReason ); |
677 | |
678 | $template = $this->getFormattedArchiveTemplate( $title ); |
679 | $template = Utils::convert( 'wikitext', 'html', $template, $title ); |
680 | |
681 | $this->editBoardDescription( |
682 | $flowArchiveTitle, |
683 | static function ( $content ) use ( $template ) { |
684 | $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain(); |
685 | $content = TemplateHelper::removeFromHtml( $content, $templateName ); |
686 | return $template . "<br/><br/>" . $content; |
687 | }, |
688 | 'html' ); |
689 | |
690 | return $flowArchiveTitle; |
691 | } |
692 | |
693 | /** |
694 | * @param array $blocks |
695 | */ |
696 | private function logBlockErrors( array $blocks ) { |
697 | $errors = []; |
698 | /** @var AbstractBlock $block */ |
699 | foreach ( $blocks as $block ) { |
700 | if ( $block->hasErrors() ) { |
701 | $blockErrors = $block->getErrors(); |
702 | foreach ( $blockErrors as $blockErrorType ) { |
703 | $errors[ $block->getName() ] = [ |
704 | 'type' => $blockErrorType, |
705 | 'message' => $block->getErrorMessage( $blockErrorType ), |
706 | 'extra' => $block->getErrorExtra( $blockErrorType ) |
707 | ]; |
708 | } |
709 | } |
710 | } |
711 | if ( $errors ) { |
712 | LoggerFactory::getInstance( 'Flow' )->error( |
713 | 'Found {count} block errors for user {user_id}', |
714 | [ |
715 | 'count' => count( $errors ), |
716 | 'user_id' => RequestContext::getMain()->getUser()->getId(), |
717 | 'errors' => FormatJson::encode( $errors ) |
718 | ] |
719 | ); |
720 | } |
721 | } |
722 | } |