Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 230 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
View | |
0.00% |
0 / 230 |
|
0.00% |
0 / 9 |
3306 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
show | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
getRobotPolicy | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
addModules | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
handleSubmit | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
buildApiResponse | |
0.00% |
0 / 75 |
|
0.00% |
0 / 1 |
240 | |||
renderApiResponse | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
182 | |||
redirect | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
extractBlockParameters | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | |
3 | namespace Flow; |
4 | |
5 | use Article; |
6 | use Flow\Block\AbstractBlock; |
7 | use Flow\Block\Block; |
8 | use Flow\Block\TopicBlock; |
9 | use Flow\Exception\InvalidActionException; |
10 | use Flow\Hooks\HookRunner; |
11 | use Flow\Model\Anchor; |
12 | use Flow\Model\HtmlRenderingInformation; |
13 | use Flow\Model\UUID; |
14 | use Flow\Model\Workflow; |
15 | use MediaWiki\Context\ContextSource; |
16 | use MediaWiki\Context\IContextSource; |
17 | use MediaWiki\Html\Html; |
18 | use MediaWiki\MediaWikiServices; |
19 | use MediaWiki\Message\Message; |
20 | use MediaWiki\Output\OutputPage; |
21 | use MediaWiki\SpecialPage\SpecialPage; |
22 | use MediaWiki\Title\Title; |
23 | |
24 | class View extends ContextSource { |
25 | /** |
26 | * @var UrlGenerator |
27 | */ |
28 | protected $urlGenerator; |
29 | |
30 | /** |
31 | * @var TemplateHelper |
32 | */ |
33 | protected $lightncandy; |
34 | |
35 | /** |
36 | * @var FlowActions |
37 | */ |
38 | protected $actions; |
39 | |
40 | public function __construct( |
41 | UrlGenerator $urlGenerator, |
42 | TemplateHelper $lightncandy, |
43 | IContextSource $requestContext, |
44 | FlowActions $actions |
45 | ) { |
46 | $this->urlGenerator = $urlGenerator; |
47 | $this->lightncandy = $lightncandy; |
48 | $this->setContext( $requestContext ); |
49 | $this->actions = $actions; |
50 | } |
51 | |
52 | public function show( WorkflowLoader $loader, $action ) { |
53 | $blocks = $loader->getBlocks(); |
54 | |
55 | $parameters = $this->extractBlockParameters( $action, $blocks ); |
56 | foreach ( $loader->getBlocks() as $block ) { |
57 | $block->init( $this, $action ); |
58 | } |
59 | |
60 | if ( $this->getRequest()->wasPosted() ) { |
61 | $retval = $this->handleSubmit( $loader, $action, $parameters ); |
62 | // successful submission |
63 | if ( $retval === true ) { |
64 | $this->redirect( $loader->getWorkflow() ); |
65 | return; |
66 | // only render the returned subset of blocks |
67 | } elseif ( is_array( $retval ) ) { |
68 | $blocks = $retval; |
69 | } |
70 | } |
71 | |
72 | $apiResponse = $this->buildApiResponse( $loader, $blocks, $action, $parameters ); |
73 | |
74 | $output = $this->getOutput(); |
75 | $output->enableOOUI(); |
76 | $this->addModules( $output, $action ); |
77 | // Please note that all blocks can set page title, which may cause them |
78 | // to override one another's titles |
79 | foreach ( $blocks as $block ) { |
80 | $block->setPageTitle( $output ); |
81 | } |
82 | |
83 | if ( $this->actions->getValue( $action, 'hasUserGeneratedContent' ) ) { |
84 | $output->setCopyright( true ); |
85 | } |
86 | |
87 | $robotPolicy = $this->getRobotPolicy( $action, $loader->getWorkflow(), $blocks ); |
88 | $this->renderApiResponse( $apiResponse, $robotPolicy ); |
89 | } |
90 | |
91 | /** |
92 | * @param string $action |
93 | * @param Workflow $workflow |
94 | * @param Block[] $blocks |
95 | * |
96 | * @return string[] |
97 | */ |
98 | private function getRobotPolicy( $action, Workflow $workflow, array $blocks ) { |
99 | if ( $action !== 'view' ) { |
100 | // consistent with 'edit' and other action pages in Core |
101 | return [ |
102 | 'index' => 'noindex', |
103 | 'follow' => 'nofollow', |
104 | ]; |
105 | } |
106 | |
107 | if ( $workflow->getType() === 'topic' ) { |
108 | /** @var TopicBlock $topic */ |
109 | $topic = $blocks[ 'topic' ]; |
110 | // @phan-suppress-next-line PhanUndeclaredMethod Cannot infer type |
111 | $topicRev = $topic->loadTopicTitle(); |
112 | if ( !$topicRev || $topicRev->isHidden() ) { |
113 | return [ |
114 | 'index' => 'noindex', |
115 | 'follow' => 'nofollow', |
116 | ]; |
117 | } |
118 | } |
119 | |
120 | $boardTitle = $workflow->getOwnerTitle(); |
121 | $article = Article::newFromTitle( $boardTitle, $this->getContext() ); |
122 | return $article->getRobotPolicy( $action ); |
123 | } |
124 | |
125 | protected function addModules( OutputPage $out, $action ) { |
126 | if ( $this->actions->hasValue( $action, 'modules' ) ) { |
127 | $out->addModules( $this->actions->getValue( $action, 'modules' ) ); |
128 | } else { |
129 | $out->addModules( [ 'ext.flow' ] ); |
130 | } |
131 | |
132 | if ( $this->actions->hasValue( $action, 'moduleStyles' ) ) { |
133 | $out->addModuleStyles( $this->actions->getValue( $action, 'moduleStyles' ) ); |
134 | } else { |
135 | $out->addModuleStyles( [ |
136 | 'mediawiki.ui', |
137 | 'mediawiki.ui.button', |
138 | 'mediawiki.ui.input', |
139 | 'mediawiki.ui.icon', |
140 | 'mediawiki.special.changeslist', |
141 | 'mediawiki.interface.helpers.styles', |
142 | 'mediawiki.editfont.styles', |
143 | 'ext.flow.styles.base', |
144 | 'ext.flow.mediawiki.ui.form', |
145 | 'oojs-ui.styles.icons-alerts', |
146 | 'oojs-ui.styles.icons-content', |
147 | 'oojs-ui.styles.icons-layout', |
148 | 'oojs-ui.styles.icons-movement', |
149 | 'oojs-ui.styles.icons-indicators', |
150 | 'oojs-ui.styles.icons-interactions', |
151 | 'oojs-ui.styles.icons-editing-core', |
152 | 'oojs-ui.styles.icons-moderation', |
153 | ] ); |
154 | } |
155 | |
156 | // Add Parsoid modules if necessary |
157 | Conversion\Utils::onFlowAddModules( $out ); |
158 | // Allow other extensions to add modules |
159 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onFlowAddModules( $out ); |
160 | } |
161 | |
162 | protected function handleSubmit( WorkflowLoader $loader, $action, array $parameters ) { |
163 | $this->getOutput()->disableClientCache(); |
164 | |
165 | $blocksToCommit = $loader->handleSubmit( $this, $action, $parameters ); |
166 | if ( !$blocksToCommit ) { |
167 | return false; |
168 | } |
169 | |
170 | if ( !$this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) ) ) { |
171 | // this uses the above $blocksToCommit reference to only render the failed blocks |
172 | foreach ( $blocksToCommit as $block ) { |
173 | $block->addError( 'edit-token', $this->msg( 'sessionfailure' ) ); |
174 | } |
175 | return $blocksToCommit; |
176 | } |
177 | |
178 | $loader->commit( $blocksToCommit ); |
179 | return true; |
180 | } |
181 | |
182 | protected function buildApiResponse( WorkflowLoader $loader, array $blocks, $action, array $parameters ) { |
183 | $workflow = $loader->getWorkflow(); |
184 | $title = $workflow->getArticleTitle(); |
185 | $user = $this->getUser(); |
186 | $categories = array_keys( $title->getParentCategories() ); |
187 | $categoryObject = []; |
188 | $linkedCategories = []; |
189 | |
190 | // Transform the raw category names into links |
191 | foreach ( $categories as $value ) { |
192 | $categoryTitle = Title::newFromText( $value ); |
193 | $categoryObject[ $value ] = [ |
194 | 'name' => $value, |
195 | 'exists' => $categoryTitle->exists() |
196 | ]; |
197 | $linkedCategories[] = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
198 | $categoryTitle, |
199 | $categoryTitle->getText() |
200 | ); |
201 | } |
202 | |
203 | // @todo This and API should use same code |
204 | $apiResponse = [ |
205 | 'title' => $title->getPrefixedText(), |
206 | 'categories' => $categoryObject, |
207 | // We need to store the link to the Special:Categories page from the |
208 | // back end php script, because there is no way in JS front end to |
209 | // get the localized link of a special page |
210 | 'specialCategoryLink' => SpecialPage::getTitleFor( 'Categories' )->getLocalURL(), |
211 | 'workflow' => $workflow->isNew() ? '' : $workflow->getId()->getAlphadecimal(), |
212 | 'blocks' => [], |
213 | // see https://phabricator.wikimedia.org/T223165 |
214 | 'isWatched' => MediaWikiServices::getInstance()->getWatchlistManager()->isWatched( $user, $title ), |
215 | 'watchable' => $user->isRegistered(), |
216 | 'links' => [ |
217 | 'watch-board' => [ |
218 | 'url' => $title->getLocalURL( 'action=watch' ), |
219 | ], |
220 | 'unwatch-board' => [ |
221 | 'url' => $title->getLocalURL( 'action=unwatch' ), |
222 | ], |
223 | ] |
224 | ]; |
225 | |
226 | $editToken = $user->getEditToken(); |
227 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
228 | $editFont = $userOptionsLookup->getOption( $user, 'editfont' ); |
229 | $wasPosted = $this->getRequest()->wasPosted(); |
230 | $topicListBlock = null; |
231 | foreach ( $blocks as $block ) { |
232 | if ( $wasPosted ? $block->canSubmit( $action ) : $block->canRender( $action ) ) { |
233 | $apiResponse['blocks'][$block->getName()] = $block->renderApi( $parameters[$block->getName()] ) |
234 | + [ |
235 | 'title' => $apiResponse['title'], |
236 | 'block-action-template' => $block->getTemplate( $action ), |
237 | 'editToken' => $editToken, |
238 | 'editFont' => $editFont, |
239 | ]; |
240 | if ( $block->getName() == 'topiclist' ) { |
241 | $topicListBlock = $block; |
242 | } |
243 | } |
244 | } |
245 | |
246 | // Add category items to the header if they exist |
247 | if ( count( $linkedCategories ) > 0 && isset( $apiResponse['blocks']['header'] ) ) { |
248 | $apiResponse['blocks']['header']['categories'] = [ |
249 | 'link' => MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
250 | SpecialPage::getTitleFor( 'Categories' ), |
251 | $this->msg( 'pagecategories' )->params( count( $linkedCategories ) )->text() |
252 | ) . $this->msg( 'colon-separator' )->escaped(), |
253 | 'items' => $linkedCategories |
254 | ]; |
255 | } |
256 | |
257 | if ( isset( $topicListBlock ) && isset( $parameters['topiclist'] ) ) { |
258 | $apiResponse['toc'] = $topicListBlock->renderTocApi( |
259 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable,PhanTypePossiblyInvalidDimOffset |
260 | $apiResponse['blocks']['topiclist'], |
261 | $parameters['topiclist'] |
262 | ); |
263 | } |
264 | |
265 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset |
266 | if ( count( $apiResponse['blocks'] ) === 0 ) { |
267 | throw new InvalidActionException( "No blocks accepted action: $action", 'invalid-action' ); |
268 | } |
269 | |
270 | array_walk_recursive( $apiResponse, static function ( &$value ) { |
271 | if ( $value instanceof Anchor ) { |
272 | $anchor = $value; |
273 | $value = $value->toArray(); |
274 | |
275 | // TODO: We're looking into another approach for this |
276 | // using a parser function, so the URL doesn't have to be |
277 | // fully qualified. |
278 | // See https://bugzilla.wikimedia.org/show_bug.cgi?id=66746 |
279 | $value['url'] = $anchor->getFullURL(); |
280 | |
281 | } elseif ( $value instanceof Message ) { |
282 | $value = $value->text(); |
283 | } elseif ( $value instanceof UUID ) { |
284 | $value = $value->getAlphadecimal(); |
285 | } |
286 | } ); |
287 | |
288 | return $apiResponse; |
289 | } |
290 | |
291 | protected function renderApiResponse( array $apiResponse, array $robotPolicy ) { |
292 | // Render the flow-component wrapper |
293 | if ( empty( $apiResponse['blocks'] ) ) { |
294 | return []; |
295 | } |
296 | |
297 | $out = $this->getOutput(); |
298 | $config = $this->getConfig(); |
299 | |
300 | $jsonBlobResponse = $apiResponse; |
301 | |
302 | // Temporary fix for T107170 |
303 | array_walk_recursive( $jsonBlobResponse, static function ( &$value, $key ) { |
304 | if ( stristr( $key, 'Token' ) !== false ) { |
305 | $value = null; |
306 | } |
307 | } ); |
308 | |
309 | // Add JSON blob for OOUI widgets |
310 | $out->addJsConfigVars( 'wgFlowData', $jsonBlobResponse ); |
311 | $out->addJsConfigVars( 'wgEditSubmitButtonLabelPublish', |
312 | $config->get( 'EditSubmitButtonLabelPublish' ) ); |
313 | |
314 | $renderedBlocks = []; |
315 | foreach ( $apiResponse['blocks'] as $block ) { |
316 | // @todo find a better way to do this; potentially make all blocks their own components |
317 | switch ( $block['type'] ) { |
318 | case 'board-history': |
319 | $flowComponent = 'boardHistory'; |
320 | $page = 'history'; |
321 | break; |
322 | case 'topic': |
323 | if ( $block['submitted']['action'] === 'history' ) { |
324 | $page = 'history'; |
325 | $flowComponent = 'boardHistory'; |
326 | } else { |
327 | $page = 'topic'; |
328 | $flowComponent = 'board'; |
329 | } |
330 | break; |
331 | default: |
332 | $flowComponent = 'board'; |
333 | $page = 'board'; |
334 | } |
335 | |
336 | if ( isset( $block['errors'] ) ) { |
337 | foreach ( $block['errors'] as $error ) { |
338 | if ( isset( $error['extra']['details'] ) && |
339 | $error['extra']['details'] instanceof HtmlRenderingInformation |
340 | ) { |
341 | $renderingInfo = $error['extra']['details']; |
342 | |
343 | $out->addHeadItems( $renderingInfo->getHeadItems() ); |
344 | $out->addModuleStyles( $renderingInfo->getModuleStyles() ); |
345 | $out->addModules( $renderingInfo->getModules() ); |
346 | } |
347 | } |
348 | } |
349 | |
350 | // Don't re-render a block type twice in one page |
351 | if ( isset( $renderedBlocks[$flowComponent] ) ) { |
352 | continue; |
353 | } |
354 | $renderedBlocks[$flowComponent] = true; |
355 | |
356 | // Get the block loop template |
357 | $template = $this->lightncandy->getTemplate( 'flow_block_loop' ); |
358 | |
359 | $classes = [ 'flow-component', "flow-$page-page" ]; |
360 | |
361 | // Always add mw-content-{ltr,rtl} class |
362 | $title = Title::newFromText( $apiResponse['title'] ); |
363 | $classes[] = 'mw-content-' . $title->getPageLanguage()->getDir(); |
364 | |
365 | $action = $this->getRequest()->getVal( 'action', 'view' ); |
366 | $classes[] = "flow-action-$action"; |
367 | |
368 | // Output the component, with the rendered blocks inside it |
369 | $out->addHTML( Html::rawElement( |
370 | 'div', |
371 | [ |
372 | 'class' => implode( ' ', $classes ), |
373 | 'data-flow-component' => $flowComponent, |
374 | 'data-flow-id' => $apiResponse['workflow'], |
375 | ], |
376 | $template( $apiResponse ) |
377 | ) ); |
378 | $out->setIndexPolicy( $robotPolicy[ 'index' ] ); |
379 | $out->setFollowPolicy( $robotPolicy[ 'follow' ] ); |
380 | } |
381 | } |
382 | |
383 | protected function redirect( Workflow $workflow ) { |
384 | $link = $this->urlGenerator->workflowLink( |
385 | $workflow->getArticleTitle(), |
386 | $workflow->getId() |
387 | ); |
388 | $this->getOutput()->redirect( $link->getFullURL() ); |
389 | } |
390 | |
391 | /** |
392 | * Helper function extracts parameters from a WebRequest. |
393 | * |
394 | * @param string $action |
395 | * @param AbstractBlock[] $blocks |
396 | * @return array |
397 | */ |
398 | public function extractBlockParameters( $action, array $blocks ) { |
399 | $request = $this->getRequest(); |
400 | $result = []; |
401 | // BC for old parameters enclosed in square brackets |
402 | foreach ( $blocks as $block ) { |
403 | $name = $block->getName(); |
404 | $result[$name] = $request->getArray( $name, [] ); |
405 | } |
406 | // BC for topic_list renamed to topiclist |
407 | if ( isset( $result['topiclist'] ) && !$result['topiclist'] ) { |
408 | $result['topiclist'] = $request->getArray( 'topic_list', [] ); |
409 | } |
410 | $globalData = [ 'action' => $action ]; |
411 | foreach ( $request->getValues() as $name => $value ) { |
412 | // between urls only allowing [-_.] as unencoded special chars and |
413 | // php mangling all of those into '_', we have to split on '_' |
414 | if ( strpos( $name, '_' ) !== false ) { |
415 | [ $block, $var ] = explode( '_', $name, 2 ); |
416 | // flow_xxx is global data for all blocks |
417 | if ( $block === 'flow' ) { |
418 | $globalData[$var] = $value; |
419 | } else { |
420 | $result[$block][$var] = $value; |
421 | } |
422 | } |
423 | } |
424 | |
425 | foreach ( $blocks as $block ) { |
426 | $result[$block->getName()] += $globalData; |
427 | } |
428 | |
429 | return $result; |
430 | } |
431 | } |