Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 230
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
View
0.00% covered (danger)
0.00%
0 / 230
0.00% covered (danger)
0.00%
0 / 9
3306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 show
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 getRobotPolicy
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 addModules
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
12
 handleSubmit
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 buildApiResponse
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
240
 renderApiResponse
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
182
 redirect
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 extractBlockParameters
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace Flow;
4
5use Article;
6use ContextSource;
7use Flow\Block\AbstractBlock;
8use Flow\Block\Block;
9use Flow\Block\TopicBlock;
10use Flow\Exception\InvalidActionException;
11use Flow\Hooks\HookRunner;
12use Flow\Model\Anchor;
13use Flow\Model\HtmlRenderingInformation;
14use Flow\Model\UUID;
15use Flow\Model\Workflow;
16use IContextSource;
17use MediaWiki\Html\Html;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Output\OutputPage;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Title\Title;
22use Message;
23
24class 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}