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