Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.17% covered (danger)
1.17%
10 / 857
2.44% covered (danger)
2.44%
1 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialCollection
1.17% covered (danger)
1.17%
10 / 857
2.44% covered (danger)
2.44%
1 / 41
52643.66
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
6
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 1
3192
 processSuggestCommand
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 processSaveCollectionCommand
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
156
 renderBookCreatorPage
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
30
 renderStopBookCreatorPage
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
2
 getBookPagePrefixes
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 renderSpecialPage
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 setTitles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setSettings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 sortByTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 sortItems
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 addChapter
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 renameChapter
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 addArticleFromName
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addArticle
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
 removeArticleFromName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 removeArticle
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 addCategoryFromName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addCategory
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 limitExceeded
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 removeItem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 moveItem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 moveItemInCollection
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 setSorting
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 parseCollectionLine
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
342
 loadCollection
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 saveCollection
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
342
 renderCollection
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 forceRenderCollection
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 renderRenderingPage
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
156
 download
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
56
 makeCollection
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 applySettings
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 postZip
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 renderSaveOverwritePage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 renderLoadOverwritePage
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 handleResult
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Collection Extension for MediaWiki
4 *
5 * Copyright (C) PediaPress GmbH
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * http://www.gnu.org/copyleft/gpl.html
21 */
22
23namespace MediaWiki\Extension\Collection\Specials;
24
25use ApiMain;
26use MediaWiki\Extension\Collection\MessageBoxHelper;
27use MediaWiki\Extension\Collection\Rendering\CollectionAPIResult;
28use MediaWiki\Extension\Collection\Rendering\CollectionRenderingAPI;
29use MediaWiki\Extension\Collection\Session as CollectionSession;
30use MediaWiki\Extension\Collection\Suggest;
31use MediaWiki\Extension\Collection\Templates\CollectionFailedTemplate;
32use MediaWiki\Extension\Collection\Templates\CollectionFinishedTemplate;
33use MediaWiki\Extension\Collection\Templates\CollectionLoadOverwriteTemplate;
34use MediaWiki\Extension\Collection\Templates\CollectionPageTemplate;
35use MediaWiki\Extension\Collection\Templates\CollectionRenderingTemplate;
36use MediaWiki\Extension\Collection\Templates\CollectionSaveOverwriteTemplate;
37use MediaWiki\Html\Html;
38use MediaWiki\MediaWikiServices;
39use MediaWiki\Request\DerivativeRequest;
40use MediaWiki\Request\WebRequest;
41use MediaWiki\SpecialPage\SpecialPage;
42use MediaWiki\Title\Title;
43use OOUI\ButtonGroupWidget;
44use OOUI\ButtonInputWidget;
45use OOUI\ButtonWidget;
46use OOUI\FormLayout;
47use RequestContext;
48use SkinTemplate;
49use UnexpectedValueException;
50
51class SpecialCollection extends SpecialPage {
52
53    /** @var resource */
54    private $tempfile;
55
56    /** @var false|array[] */
57    private $mPODPartners;
58
59    /**
60     * @param false|array[] $PODPartners
61     */
62    public function __construct( $PODPartners = false ) {
63        parent::__construct( "Book" );
64        global $wgCollectionPODPartners;
65        if ( $PODPartners ) {
66            $this->mPODPartners = $PODPartners;
67        } else {
68            $this->mPODPartners = $wgCollectionPODPartners;
69        }
70    }
71
72    public function doesWrites() {
73        return true;
74    }
75
76    /**
77     * @return string
78     */
79    public function getDescription() {
80        return $this->msg( 'coll-collection' );
81    }
82
83    /**
84     * @param null|string $par
85     */
86    public function execute( $par ) {
87        global $wgCollectionMaxArticles;
88
89        $out = $this->getOutput();
90        $request = $this->getRequest();
91
92        // support previous URLs (e.g. used in templates) which used the "$par" part
93        // (i.e. subpages of the Special page)
94        if ( $par ) {
95            if ( $request->wasPosted() ) { // don't redirect POST reqs
96                // TODO
97            }
98            $out->redirect( wfAppendQuery(
99                SkinTemplate::makeSpecialUrl( 'Book' ),
100                $request->appendQueryArray( [ 'bookcmd' => rtrim( $par, '/' ) ] )
101            ) );
102            return;
103        }
104
105        switch ( $request->getVal( 'bookcmd', '' ) ) {
106            case 'book_creator':
107                $this->renderBookCreatorPage( $request->getVal( 'referer', '' ), $par );
108                return;
109
110            case 'start_book_creator':
111                $title = Title::newFromText( $request->getVal( 'referer', '' ) );
112                if ( $title === null ) {
113                    $title = Title::newMainPage();
114                }
115                if ( $request->getVal( 'confirm' ) ) {
116                    CollectionSession::enable();
117                }
118                $out->redirect( $title->getFullURL() );
119                return;
120
121            case 'stop_book_creator':
122                $title = Title::newFromText( $request->getVal( 'referer', '' ) );
123                if ( $title === null || $title->equals( $this->getPageTitle( $par ) ) ) {
124                    $title = Title::newMainPage();
125                }
126                if ( $request->getVal( 'confirm' ) ) {
127                    CollectionSession::disable();
128                } elseif ( !$request->getVal( 'continue' ) ) {
129                    $this->renderStopBookCreatorPage( $title );
130                    return;
131                }
132                $out->redirect( $title->getFullURL() );
133                return;
134
135            case 'add_article':
136                if ( CollectionSession::countArticles() >= $wgCollectionMaxArticles ) {
137                    self::limitExceeded();
138                    return;
139                }
140                $oldid = $request->getInt( 'oldid', 0 );
141                $title = Title::newFromText( $request->getVal( 'arttitle', '' ) );
142                if ( !$title ) {
143                    return;
144                }
145                if ( self::addArticle( $title, $oldid ) ) {
146                    if ( $oldid == 0 ) {
147                        $redirectURL = $title->getFullURL();
148                    } else {
149                        $redirectURL = $title->getFullURL( 'oldid=' . $oldid );
150                    }
151                    $out->redirect( $redirectURL );
152                } else {
153                    $out->showErrorPage(
154                        'coll-couldnotaddarticle_title',
155                        'coll-couldnotaddarticle_msg'
156                    );
157                }
158                return;
159
160            case 'remove_article':
161                $oldid = $request->getInt( 'oldid', 0 );
162                $title = Title::newFromText( $request->getVal( 'arttitle', '' ) );
163                if ( !$title ) {
164                    return;
165                }
166                if ( self::removeArticle( $title, $oldid ) ) {
167                    if ( $oldid == 0 ) {
168                        $redirectURL = $title->getFullURL();
169                    } else {
170                        $redirectURL = $title->getFullURL( 'oldid=' . $oldid );
171                    }
172                    $out->redirect( $redirectURL );
173                } else {
174                    $out->showErrorPage(
175                        'coll-couldnotremovearticle_title',
176                        'coll-couldnotremovearticle_msg'
177                    );
178                }
179                return;
180
181            case 'clear_collection':
182                CollectionSession::clearCollection();
183                $redirect = $request->getVal( 'return_to', '' );
184                $redirectURL = SkinTemplate::makeSpecialUrl( 'Book' );
185                if ( $redirect !== '' ) {
186                    $title = Title::newFromText( $redirect );
187                    if ( $title ) {
188                        $redirectURL = $title->getFullURL();
189                    }
190                }
191                $out->redirect( $redirectURL );
192                return;
193
194            case 'set_titles':
195                self::setTitles(
196                    $request->getText( 'collectionTitle', '' ),
197                    $request->getText( 'collectionSubtitle', '' )
198                );
199                $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
200                return;
201
202            case 'sort_items':
203                self::sortItems();
204                $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
205                return;
206
207            case 'add_category':
208                $title = Title::makeTitleSafe( NS_CATEGORY, $request->getVal( 'cattitle', '' ) );
209                if ( !$title ) {
210                    return;
211                } elseif ( self::addCategory( $title ) ) {
212                    self::limitExceeded();
213                    return;
214                } else {
215                    $out->redirect( $request->getVal( 'return_to', $title->getFullURL() ) );
216                }
217                return;
218
219            case 'remove_item':
220                self::removeItem( $request->getInt( 'index', 0 ) );
221                $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
222                return;
223
224            case 'move_item':
225                self::moveItem( $request->getInt( 'index', 0 ), $request->getInt( 'delta', 0 ) );
226                $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
227                return;
228
229            case 'load_collection':
230                $title = Title::newFromText( $request->getVal( 'colltitle', '' ) );
231                if ( !$title ) {
232                    return;
233                }
234                if ( $request->getVal( 'cancel' ) ) {
235                    $out->redirect( $title->getFullURL() );
236                    return;
237                }
238                if ( !CollectionSession::countArticles()
239                    || $request->getVal( 'overwrite' )
240                    || $request->getVal( 'append' )
241                ) {
242                    $collection = $this->loadCollection( $title, $request->getBool( 'append' ) );
243                    if ( $collection ) {
244                        CollectionSession::startSession();
245                        CollectionSession::setCollection( $collection );
246                        CollectionSession::enable();
247                        $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
248                    }
249                    return;
250                }
251                $this->renderLoadOverwritePage( $title );
252                return;
253
254            case 'order_collection':
255                $title = Title::newFromText( $request->getVal( 'colltitle', '' ) );
256                if ( !$title ) {
257                    return;
258                }
259                $collection = $this->loadCollection( $title );
260                if ( $collection ) {
261                    $partner = $request->getVal( 'partner', key( $this->mPODPartners ) );
262                    $this->postZip( $collection, $partner );
263                }
264                return;
265
266            case 'save_collection':
267                $this->processSaveCollectionCommand();
268                return;
269
270            case 'render':
271                $this->renderCollection(
272                    CollectionSession::getCollection(),
273                    SpecialPage::getTitleFor( 'Book' ),
274                    $request->getVal( 'writer', '' )
275                );
276                return;
277
278            case 'forcerender':
279                $this->forceRenderCollection();
280                return;
281
282            case 'rendering':
283                $this->renderRenderingPage();
284                return;
285
286            case 'download':
287                $this->download();
288                return;
289
290            case 'render_article':
291                $title = Title::newFromText( $request->getVal( 'arttitle', '' ) );
292                if ( !$title ) {
293                    return;
294                }
295                $oldid = $request->getInt( 'oldid', 0 );
296                $collection = $this->makeCollection( $title, $oldid );
297                if ( $collection ) {
298                    $this->applySettings( $collection, $request );
299                    $this->renderCollection( $collection, $title, $request->getVal( 'writer', 'rl' ) );
300                }
301                return;
302
303            case 'render_collection':
304                $title = Title::newFromText( $request->getVal( 'colltitle', '' ) );
305                if ( !$title ) {
306                    return;
307                }
308                $collection = $this->loadCollection( $title );
309                if ( $collection ) {
310                    $this->applySettings( $collection, $request );
311                    $this->renderCollection( $collection, $title, $request->getVal( 'writer', 'rl' ) );
312                }
313                return;
314
315            case 'post_zip':
316                $partner = $request->getVal( 'partner', 'pediapress' );
317                $this->postZip( CollectionSession::getCollection(), $partner );
318                return;
319
320            case 'suggest':
321                $this->processSuggestCommand();
322                return;
323
324            case '':
325                $this->renderSpecialPage();
326                return;
327
328            default:
329                $out->showErrorPage( 'coll-unknown_subpage_title', 'coll-unknown_subpage_text' );
330        }
331    }
332
333    /**
334     * Processes the suggest command
335     */
336    private function processSuggestCommand() {
337        $request = $this->getRequest();
338
339        $add = $request->getVal( 'add' );
340        $ban = $request->getVal( 'ban' );
341        $remove = $request->getVal( 'remove' );
342        $addselected = $request->getVal( 'addselected' );
343
344        if ( $request->getVal( 'resetbans' ) ) {
345            Suggest::run( 'resetbans' );
346        } elseif ( $add !== null ) {
347            Suggest::run( 'add', $add );
348        } elseif ( $ban !== null ) {
349            Suggest::run( 'ban', $ban );
350        } elseif ( $remove !== null ) {
351            Suggest::run( 'remove', $remove );
352        } elseif ( $addselected !== null ) {
353            $articleList = $request->getArray( 'articleList' );
354            if ( $articleList !== null ) {
355                Suggest::run( 'addAll', $articleList );
356            } else {
357                Suggest::run();
358            }
359        } else {
360            Suggest::run();
361        }
362    }
363
364    /**
365     * Processes the save book command
366     */
367    private function processSaveCollectionCommand() {
368        $out = $this->getOutput();
369        $request = $this->getRequest();
370        $user = $this->getUser();
371
372        if ( $request->getVal( 'abort' ) ) {
373            $out->redirect( SkinTemplate::makeSpecialUrl( 'Book' ) );
374            return;
375        }
376        if ( !$user->matchEditToken( $request->getVal( 'token' ) ) ) {
377            return;
378        }
379
380        $colltype = $request->getVal( 'colltype' );
381        $prefixes = $this->getBookPagePrefixes();
382        $title = null;
383        if ( $colltype == 'personal' ) {
384            $collname = $request->getVal( 'pcollname', '' );
385            if ( !$user->isAllowed( 'collectionsaveasuserpage' ) || $collname === '' ) {
386                return;
387            }
388            $title = Title::newFromText( $prefixes['user-prefix'] . $collname );
389        } elseif ( $colltype == 'community' ) {
390            $collname = $request->getVal( 'ccollname', '' );
391            if ( !$user->isAllowed( 'collectionsaveascommunitypage' ) || $collname === '' ) {
392                return;
393            }
394            $title = Title::newFromText( $prefixes['community-prefix'] . $collname );
395        }
396        if ( !$title || !$colltype ) {
397            return;
398        }
399
400        if ( $this->saveCollection( $title, $request->getBool( 'overwrite' ) ) ) {
401            $out->redirect( $title->getFullURL() );
402        } else {
403            $this->renderSaveOverwritePage(
404                $colltype,
405                $title,
406                $request->getVal( 'pcollname' ),
407                $request->getVal( 'ccollname' )
408            );
409        }
410    }
411
412    /**
413     * @param string $referer
414     * @param string $par
415     */
416    private function renderBookCreatorPage( $referer, $par ) {
417        $out = $this->getOutput();
418        $out->enableOOUI();
419
420        $this->setHeaders();
421        $out->setPageTitleMsg( $this->msg( 'coll-book_creator' ) );
422
423        MessageBoxHelper::addModuleStyles( $out );
424        $out->addHTML( MessageBoxHelper::renderWarningBoxes() );
425        $out->addWikiMsg( 'coll-book_creator_intro' );
426
427        $out->addModules( 'ext.collection.checkLoadFromLocalStorage' );
428
429        $title = Title::newFromText( $referer );
430        if ( $title === null || $title->equals( $this->getPageTitle( $par ) ) ) {
431            $title = Title::newMainPage();
432        }
433
434        $form = new FormLayout( [
435            'method' => 'POST',
436            'action' => SkinTemplate::makeSpecialUrl(
437                'Book',
438                [
439                    'bookcmd' => 'start_book_creator',
440                    'referer' => $referer,
441                ]
442            ),
443        ] );
444        $form->appendContent( new ButtonGroupWidget( [
445            'items' => [
446                new ButtonInputWidget( [
447                    'type' => 'submit',
448                    'name' => 'confirm',
449                    'value' => 'yes',
450                    'flags' => [ 'primary', 'progressive' ],
451                    'label' => $this->msg( 'coll-start_book_creator' )->text(),
452                ] ),
453                new ButtonWidget( [
454                    'href' => $title->getLinkURL(),
455                    'title' => $title->getPrefixedText(),
456                    'label' => $this->msg( 'coll-cancel' )->text(),
457                    'noFollow' => true,
458                ] ),
459            ],
460        ] ) );
461
462        $out->addHTML( $form );
463
464        $title_string = $this->msg( 'coll-book_creator_text_article' )->inContentLanguage()->text();
465        $t = Title::newFromText( $title_string );
466        if ( $t !== null ) {
467            if ( $t->exists() ) {
468                $out->addWikiTextAsInterface( '{{:' . $title_string . '}}' );
469                return;
470            }
471        }
472        $out->addWikiMsg( 'coll-book_creator_help' );
473    }
474
475    /**
476     * @param string $referer
477     */
478    private function renderStopBookCreatorPage( $referer ) {
479        $out = $this->getOutput();
480        $out->enableOOUI();
481
482        $this->setHeaders();
483        $out->setPageTitleMsg( $this->msg( 'coll-book_creator_disable' ) );
484        $out->addWikiMsg( 'coll-book_creator_disable_text' );
485
486        $form = new FormLayout( [
487            'method' => 'POST',
488            'action' => SkinTemplate::makeSpecialUrl(
489                'Book',
490                [
491                    'bookcmd' => 'stop_book_creator',
492                    'referer' => $referer,
493                ]
494            ),
495        ] );
496        $form->appendContent( new ButtonGroupWidget( [
497            'items' => [
498                new ButtonInputWidget( [
499                    'type' => 'submit',
500                    'name' => 'continue',
501                    'value' => 'yes',
502                    'label' => $this->msg( 'coll-book_creator_continue' )->text(),
503                ] ),
504                new ButtonInputWidget( [
505                    'type' => 'submit',
506                    'name' => 'confirm',
507                    'value' => 'yes',
508                    'label' => $this->msg( 'coll-book_creator_disable' )->text(),
509                    'flags' => [ 'primary', 'destructive' ],
510                ] ),
511            ],
512        ] ) );
513
514        $out->addHTML( $form );
515    }
516
517    /**
518     * @return array
519     */
520    private function getBookPagePrefixes() {
521        $result = [];
522        $user = $this->getUser();
523        $communityCollectionNamespace = $this->getConfig()->get( 'CommunityCollectionNamespace' );
524
525        $t = $this->msg( 'coll-user_book_prefix', $user->getName() )->inContentLanguage();
526        if ( $t->isDisabled() ) {
527            $userPageTitle = $user->getUserPage()->getPrefixedText();
528            $result['user-prefix'] = $userPageTitle . '/'
529                . $this->msg( 'coll-collections' )->inContentLanguage()->text() . '/';
530        } else {
531            $result['user-prefix'] = $t->text();
532        }
533
534        $comBookPrefix = $this->msg( 'coll-community_book_prefix' )->inContentLanguage();
535        if ( $comBookPrefix->isDisabled() ) {
536            $title = Title::makeTitle(
537                $communityCollectionNamespace,
538                $this->msg( 'coll-collections' )->inContentLanguage()->text()
539            );
540            $result['community-prefix'] = $title->getPrefixedText() . '/';
541        } else {
542            $result['community-prefix'] = $comBookPrefix->text();
543        }
544        return $result;
545    }
546
547    private function renderSpecialPage() {
548        global $wgCollectionFormats, $wgCollectionRendererSettings,
549            $wgCollectionDisableDownloadSection;
550
551        if ( !CollectionSession::hasSession() ) {
552            CollectionSession::startSession();
553        }
554
555        $out = $this->getOutput();
556        MessageBoxHelper::addModuleStyles( $out );
557
558        $this->setHeaders();
559        $this->addHelpLink( 'Special:MyLanguage/Extension:Collection/Help' );
560        $out->setPageTitleMsg( $this->msg( 'coll-manage_your_book' ) );
561        $out->addModules( 'ext.collection' );
562        $out->addModuleStyles( [ 'mediawiki.hlist', 'ext.collection.bookcreator.styles' ] );
563        $out->addJsConfigVars( [
564            'wgCollectionDisableDownloadSection' => $wgCollectionDisableDownloadSection
565        ] );
566
567        $template = new CollectionPageTemplate();
568        $template->set( 'context', $this->getContext() );
569        $template->set( 'collection', CollectionSession::getCollection() );
570        $template->set( 'podpartners', $this->mPODPartners );
571        $template->set( 'settings', $wgCollectionRendererSettings );
572        $template->set( 'formats', $wgCollectionFormats );
573        $prefixes = $this->getBookPagePrefixes();
574        $template->set( 'user-book-prefix', $prefixes['user-prefix'] );
575        $template->set( 'community-book-prefix', $prefixes['community-prefix'] );
576        $out->addTemplate( $template );
577    }
578
579    /**
580     * @param string $title
581     * @param string $subtitle
582     */
583    public static function setTitles( $title, $subtitle ) {
584        $collection = CollectionSession::getCollection();
585        $collection['title'] = $title;
586        $collection['subtitle'] = $subtitle;
587        CollectionSession::setCollection( $collection );
588    }
589
590    /**
591     * @param array $settings
592     */
593    public static function setSettings( array $settings ) {
594        $collection = CollectionSession::getCollection();
595        if ( !isset( $collection['settings'] ) ) {
596            $collection['settings'] = [];
597        }
598        $collection['settings'] = $settings + $collection['settings'];
599        CollectionSession::setCollection( $collection );
600    }
601
602    /**
603     * @param array &$items
604     */
605    private static function sortByTitle( array &$items ) {
606        usort( $items, static function ( $a, $b ) {
607            return strcasecmp( $a['title'], $b['title'] );
608        } );
609    }
610
611    public static function sortItems() {
612        $collection = CollectionSession::getCollection();
613        if ( !isset( $collection['items'] ) || !is_array( $collection['items'] ) ) {
614            $collection['items'] = [];
615            CollectionSession::setCollection( $collection );
616            return;
617        }
618
619        $articles = [];
620        $new_items = [];
621        foreach ( $collection['items'] as $item ) {
622            '@phan-var array $item';
623            if ( $item['type'] == 'chapter' ) {
624                self::sortByTitle( $articles );
625                $new_items = array_merge( $new_items, $articles, [ $item ] );
626                $articles = [];
627            } elseif ( $item['type'] == 'article' ) {
628                $articles[] = $item;
629            }
630        }
631        self::sortByTitle( $articles );
632        $collection['items'] = array_merge( $new_items, $articles );
633        CollectionSession::setCollection( $collection );
634    }
635
636    /**
637     * @param string $name
638     */
639    public static function addChapter( $name ) {
640        $collection = CollectionSession::getCollection();
641        if ( !isset( $collection['items'] ) || !is_array( $collection['items'] ) ) {
642            $collection['items'] = [];
643        }
644        array_push( $collection['items'], [
645            'type' => 'chapter',
646            'title' => $name,
647        ] );
648        CollectionSession::setCollection( $collection );
649    }
650
651    /**
652     * @param int $index
653     * @param string $name
654     */
655    public static function renameChapter( $index, $name ) {
656        if ( !is_int( $index ) ) {
657            return;
658        }
659        $collection = CollectionSession::getCollection();
660
661        // T293261: Make sure the index exist in the array
662        if ( !array_key_exists( $index, $collection['items'] ) ||
663            $collection['items'][$index]['type'] !== 'chapter'
664        ) {
665            return;
666        }
667
668        $collection['items'][$index]['title'] = $name;
669        CollectionSession::setCollection( $collection );
670    }
671
672    /**
673     * @param int $namespace
674     * @param string $name
675     * @param int $oldid
676     * @return bool
677     */
678    public static function addArticleFromName( $namespace, $name, $oldid = 0 ) {
679        $title = Title::makeTitleSafe( $namespace, $name );
680        if ( !$title ) {
681            return false;
682        }
683        return self::addArticle( $title, $oldid );
684    }
685
686    /**
687     * @param Title $title
688     * @param int $oldid
689     * @return bool
690     */
691    private static function addArticle( $title, $oldid = 0 ) {
692        $latest = $title->getLatestRevID();
693
694        $currentVersion = 0;
695        if ( $oldid == 0 ) {
696            $currentVersion = 1;
697            $oldid = $latest;
698        }
699
700        $prefixedText = $title->getPrefixedText();
701
702        $index = CollectionSession::findArticle( $prefixedText, $oldid );
703        if ( $index != -1 ) {
704            return false;
705        }
706
707        if ( !CollectionSession::hasSession() ) {
708            CollectionSession::startSession();
709        }
710        $collection = CollectionSession::getCollection();
711        $revision = MediaWikiServices::getInstance()
712            ->getRevisionLookup()
713            ->getRevisionByTitle( $title, $oldid );
714        if ( !$revision ) {
715            return false;
716        }
717
718        $item = [
719            'type' => 'article',
720            'content_type' => 'text/x-wiki',
721            'title' => $prefixedText,
722            'revision' => strval( $oldid ),
723            'latest' => strval( $latest ),
724            'timestamp' => wfTimestamp( TS_UNIX, $revision->getTimestamp() ),
725            'url' => $title->getCanonicalURL(),
726            'currentVersion' => $currentVersion,
727        ];
728
729        $collection['items'][] = $item;
730        CollectionSession::setCollection( $collection );
731        return true;
732    }
733
734    /**
735     * @param int $namespace
736     * @param string $name
737     * @param int $oldid
738     * @return bool
739     */
740    public static function removeArticleFromName( $namespace, $name, $oldid = 0 ) {
741        $title = Title::makeTitleSafe( $namespace, $name );
742        return self::removeArticle( $title, $oldid );
743    }
744
745    /**
746     * @param Title $title
747     * @param int $oldid
748     * @return bool
749     */
750    private static function removeArticle( $title, $oldid = 0 ) {
751        if ( !CollectionSession::hasSession() || !$title ) {
752            return false;
753        }
754        $collection = CollectionSession::getCollection();
755        $index = CollectionSession::findArticle( $title->getPrefixedText(), $oldid );
756        if ( $index != -1 ) {
757            array_splice( $collection['items'], $index, 1 );
758        }
759        CollectionSession::setCollection( $collection );
760        return true;
761    }
762
763    /**
764     * @param string $name
765     * @return bool
766     */
767    public static function addCategoryFromName( $name ) {
768        $title = Title::makeTitleSafe( NS_CATEGORY, $name );
769        return self::addCategory( $title );
770    }
771
772    /**
773     * @param Title $title
774     * @return bool
775     */
776    private static function addCategory( $title ) {
777        global $wgCollectionMaxArticles, $wgCollectionArticleNamespaces;
778
779        $limit = $wgCollectionMaxArticles - CollectionSession::countArticles();
780        if ( $limit <= 0 || !$title ) {
781            self::limitExceeded();
782            return false;
783        }
784        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
785        $tables = [ 'page', 'categorylinks' ];
786        $fields = [ 'page_namespace', 'page_title' ];
787        $options = [
788            'ORDER BY' => 'cl_type, cl_sortkey',
789            'LIMIT' => $limit + 1,
790        ];
791        $where = [
792            'cl_from=page_id',
793            'cl_to' => $title->getDBkey(),
794        ];
795        $res = $dbr->select( $tables, $fields, $where, __METHOD__, $options );
796
797        $count = 0;
798        $limitExceeded = false;
799        foreach ( $res as $row ) {
800            if ( ++$count > $limit ) {
801                $limitExceeded = true;
802                break;
803            }
804            if ( in_array( $row->page_namespace, $wgCollectionArticleNamespaces ) ) {
805                $articleTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
806                if ( CollectionSession::findArticle( $articleTitle->getPrefixedText() ) == -1 ) {
807                    self::addArticle( $articleTitle );
808                }
809            }
810        }
811        return $limitExceeded;
812    }
813
814    private static function limitExceeded() {
815        $out = RequestContext::getMain()->getOutput();
816        $out->showErrorPage( 'coll-limit_exceeded_title', 'coll-limit_exceeded_text' );
817    }
818
819    /**
820     * @param int $index
821     * @return bool
822     */
823    public static function removeItem( $index ) {
824        if ( !is_int( $index ) ) {
825            return false;
826        }
827        if ( !CollectionSession::hasSession() ) {
828            return false;
829        }
830        $collection = CollectionSession::getCollection();
831        array_splice( $collection['items'], $index, 1 );
832        CollectionSession::setCollection( $collection );
833        return true;
834    }
835
836    /**
837     * @param int $index
838     * @param int $delta
839     * @return bool
840     */
841    private static function moveItem( $index, $delta ) {
842        if ( !CollectionSession::hasSession() ) {
843            return false;
844        }
845        $collection = CollectionSession::getCollection();
846        $collection = self::moveItemInCollection( $collection, $index, $delta );
847        if ( $collection === false ) {
848            return false;
849        } else {
850            CollectionSession::setCollection( $collection );
851            return true;
852        }
853    }
854
855    /**
856     * @param array $collection
857     * @param int $index
858     * @param int $delta
859     * @return array|false
860     */
861    public static function moveItemInCollection( array $collection, $index, $delta ) {
862        $swapIndex = $index + $delta;
863        if ( !$collection || !isset( $collection['items'] ) ) {
864            return false;
865        }
866        $items = $collection['items'];
867        if ( isset( $items[$swapIndex] ) && isset( $items[$index] ) ) {
868            $saved = $items[$swapIndex];
869            $collection['items'][$swapIndex] = $items[$index];
870            $collection['items'][$index] = $saved;
871            return $collection;
872        } else {
873            return false;
874        }
875    }
876
877    /**
878     * @param array<int,int> $items Mapping new to old positions, missing positions will be deleted
879     */
880    public static function setSorting( array $items ) {
881        if ( !CollectionSession::hasSession() ) {
882            return;
883        }
884        $collection = CollectionSession::getCollection();
885        $old_items = $collection['items'];
886        $new_items = [];
887        foreach ( $items as $new_index => $old_index ) {
888            // Fail-safe when the "setsorting" API is hit multiple times, but an old item is already
889            // deleted
890            if ( isset( $old_items[$old_index] ) ) {
891                $new_items[$new_index] = $old_items[$old_index];
892            }
893        }
894        $collection['items'] = $new_items;
895        CollectionSession::setCollection( $collection );
896    }
897
898    /**
899     * @param array &$collection
900     * @param string $line
901     * @param bool $append
902     * @return array|null
903     */
904    private function parseCollectionLine( &$collection, $line, $append ) {
905        $line = trim( $line );
906        if ( !$append && preg_match( '/^===\s*(.*?)\s*===$/', $line, $match ) ) {
907            $collection['subtitle'] = $match[ 1 ];
908        } elseif ( !$append && preg_match( '/^==\s*(.*?)\s*==$/', $line, $match ) ) {
909            $collection['title'] = $match[ 1 ];
910        } elseif (
911            !$append &&
912            preg_match( '/^\s*\|\s*setting-([a-zA-Z0-9_-]+)\s*=\s*([^|]*)\s*$/', $line, $match )
913        ) {
914            $collection['settings'][$match[ 1 ]] = $match[ 2 ];
915        } elseif ( substr( $line, 0, 1 ) == ';' ) { // chapter
916            return [
917                'type' => 'chapter',
918                'title' => trim( substr( $line, 1 ) ),
919            ];
920        } elseif ( substr( $line, 0, 1 ) == ':' ) { // article
921            $articleTitle = trim( substr( $line, 1 ) );
922            if ( preg_match( '/^\[\[:?(.*?)(\|(.*?))?\]\]$/', $articleTitle, $match ) ) {
923                $articleTitle = $match[1];
924                if ( isset( $match[3] ) ) {
925                    $displayTitle = $match[3];
926                } else {
927                    $displayTitle = null;
928                }
929                $oldid = 0;
930                $currentVersion = 1;
931            } elseif (
932                preg_match( '/^\[\{\{fullurl:(.*?)\|oldid=(.*?)\}\}\s+(.*?)\]$/', $articleTitle, $match )
933            ) {
934                $articleTitle = $match[1];
935                if ( isset( $match[3] ) ) {
936                    $displayTitle = $match[3];
937                } else {
938                    $displayTitle = null;
939                }
940                $oldid = (int)$match[2];
941                $currentVersion = 0;
942            } else {
943                return null;
944            }
945
946            $articleTitle = Title::newFromText( $articleTitle );
947            if ( !$articleTitle ) {
948                return null;
949            }
950
951            if ( !$articleTitle->exists() ) {
952                return null;
953            }
954
955            $revision = MediaWikiServices::getInstance()
956                ->getRevisionLookup()
957                ->getRevisionByTitle( $articleTitle, $oldid );
958            if ( !$revision ) {
959                return null;
960            }
961            $latest = $articleTitle->getLatestRevID();
962
963            if ( !$oldid ) {
964                $oldid = $latest;
965            }
966
967            $d = [
968                'type' => 'article',
969                'content_type' => 'text/x-wiki',
970                'title' => $articleTitle->getPrefixedText(),
971                'latest' => $latest,
972                'revision' => $oldid,
973                'timestamp' => wfTimestamp( TS_UNIX, $revision->getTimestamp() ),
974                'url' => $articleTitle->getCanonicalURL(),
975                'currentVersion' => $currentVersion,
976            ];
977            if ( $displayTitle ) {
978                $d['displaytitle'] = $displayTitle;
979            }
980            return $d;
981        }
982        return null;
983    }
984
985    /**
986     * @param Title $title
987     * @param bool $append
988     * @return array|false
989     */
990    private function loadCollection( Title $title, $append = false ) {
991        $out = $this->getOutput();
992
993        if ( !$title->exists() ) {
994            $out->showErrorPage( 'coll-notfound_title', 'coll-notfound_text' );
995            return false;
996        }
997
998        if ( !$append || !CollectionSession::hasSession() ) {
999            $collection = [
1000                'title' => '',
1001                'subtitle' => '',
1002                'settings' => [],
1003            ];
1004            $items = [];
1005        } else {
1006            $collection = CollectionSession::getCollection();
1007            $items = $collection['items'];
1008        }
1009
1010        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
1011        $lines = preg_split(
1012            '/[\r\n]+/',
1013            $page->getContent()->getNativeData()
1014        );
1015
1016        foreach ( $lines as $line ) {
1017            $item = $this->parseCollectionLine( $collection, $line, $append );
1018            if ( $item !== null ) {
1019                $items[] = $item;
1020            }
1021        }
1022        $collection['items'] = $items;
1023        return $collection;
1024    }
1025
1026    /**
1027     * @param Title $title
1028     * @param bool $forceOverwrite
1029     * @return bool
1030     */
1031    private function saveCollection( Title $title, $forceOverwrite = false ) {
1032        if ( $title->exists() && !$forceOverwrite ) {
1033            return false;
1034        }
1035
1036        $collection = CollectionSession::getCollection();
1037        $articleText = "{{" . $this->msg( 'coll-savedbook_template' )->inContentLanguage()->text();
1038        if ( !empty( $collection['settings'] ) ) {
1039            $articleText .= "\n";
1040            foreach ( $collection['settings'] as $key => $value ) {
1041                $articleText .= " | setting-$key = $value\n";
1042            }
1043        }
1044        $articleText .= "}}\n\n";
1045        if ( $collection['title'] ) {
1046            $articleText .= '== ' . $collection['title'] . " ==\n";
1047        }
1048        if ( $collection['subtitle'] ) {
1049            $articleText .= '=== ' . $collection['subtitle'] . " ===\n";
1050        }
1051        if ( !empty( $collection['items'] ) ) {
1052            foreach ( $collection['items'] as $item ) {
1053                if ( $item['type'] == 'chapter' ) {
1054                    $articleText .= ';' . $item['title'] . "\n";
1055                } elseif ( $item['type'] == 'article' ) {
1056                    if ( $item['currentVersion'] == 1 ) {
1057                        $articleText .= ":[[" . $item['title'];
1058                        if ( isset( $item['displaytitle'] ) && $item['displaytitle'] ) {
1059                            $articleText .= "|" . $item['displaytitle'];
1060                        }
1061                        $articleText .= "]]\n";
1062                    } else {
1063                        $articleText .= ":[{{fullurl:" . $item['title'];
1064                        $articleText .= "|oldid=" . $item['revision'] . "}} ";
1065                        if ( isset( $item['displaytitle'] ) && $item['displaytitle'] ) {
1066                            $articleText .= $item['displaytitle'];
1067                        } else {
1068                            $articleText .= $item['title'];
1069                        }
1070                        $articleText .= "]\n";
1071                    }
1072                }
1073                // $articleText .= $item['revision'] . "/" . $item['latest']."\n";
1074            }
1075        }
1076        $t = $this->msg( 'coll-bookscategory' )->inContentLanguage();
1077        if ( !$t->isDisabled() ) {
1078            $catTitle = Title::makeTitle( NS_CATEGORY, $t->text() );
1079            if ( $catTitle !== null ) {
1080                $articleText .= "\n[[" . $catTitle->getPrefixedText() .
1081                    "|" . wfEscapeWikiText( $title->getSubpageText() ) . "]]\n";
1082            }
1083        }
1084
1085        $req = new DerivativeRequest(
1086            $this->getRequest(),
1087            [
1088                'action' => 'edit',
1089                'title' => $title->getPrefixedText(),
1090                'text' => $articleText,
1091                'token' => $this->getUser()->getEditToken(),
1092            ],
1093            true
1094        );
1095        $api = new ApiMain( $req, true );
1096        $api->execute();
1097        return true;
1098    }
1099
1100    /**
1101     * Take an array of arrays, each containing information about one item to be
1102     * assembled and exported, and appropriately feed the backend chosen ($writer).
1103     * @param array $collection following the collection/Metabook dictionary formats
1104     * https://www.mediawiki.org/wiki/Offline_content_generator/metabook.json
1105     * https://mwlib.readthedocs.org/en/latest/internals.html#article
1106     * @param Title $referrer Used only to provide a returnto parameter.
1107     * @param string $writer A writer registered in the appropriate configuration.
1108     */
1109    private function renderCollection( array $collection, Title $referrer, $writer ) {
1110        if ( !$writer ) {
1111            $writer = 'rl';
1112        }
1113
1114        $api = CollectionRenderingAPI::instance( $writer );
1115        $response = $api->render( $collection );
1116
1117        if ( !$this->handleResult( $response ) ) {
1118            return;
1119        }
1120
1121        $query = 'bookcmd=rendering'
1122            . '&return_to=' . urlencode( $referrer->getPrefixedText() )
1123            . '&collection_id=' . urlencode( $response->get( 'collection_id' ) )
1124            . '&writer=' . urlencode( $writer );
1125        if ( $response->get( 'is_cached' ) ) {
1126            $query .= '&is_cached=1';
1127        }
1128        $redirect = SkinTemplate::makeSpecialUrl( 'Book', $query );
1129        $this->getOutput()->redirect( $redirect );
1130    }
1131
1132    private function forceRenderCollection() {
1133        $request = $this->getRequest();
1134
1135        $collectionID = $request->getVal( 'collection_id', '' );
1136        $writer = $request->getVal( 'writer', 'rl' );
1137
1138        $api = CollectionRenderingAPI::instance( $writer );
1139        $response = $api->forceRender( $collectionID );
1140
1141        if ( !$response || $response->isError() ) {
1142            return;
1143        }
1144
1145        $query = 'bookcmd=rendering'
1146            . '&return_to=' . urlencode( $request->getVal( 'return_to', '' ) )
1147            . '&collection_id=' . urlencode( $response->get( 'collection_id' ) )
1148            . '&writer=' . urlencode( $response->get( 'writer' ) );
1149        if ( $response->get( 'is_cached' ) ) {
1150            $query .= '&is_cached=1';
1151        }
1152        $this->getOutput()->redirect( SkinTemplate::makeSpecialUrl( 'Book', $query ) );
1153    }
1154
1155    private function renderRenderingPage() {
1156        $this->setHeaders();
1157        $request = $this->getRequest();
1158        $out = $this->getOutput();
1159        $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1160
1161        $collectionId = $request->getVal( 'collection_id' );
1162        $writer = $request->getVal( 'writer' );
1163        $return_to = $request->getVal( 'return_to', '' );
1164
1165        $result = CollectionRenderingAPI::instance( $writer )->getRenderStatus( $collectionId );
1166        if ( !$this->handleResult( $result ) ) {
1167            return; // FIXME?
1168        }
1169
1170        $query = 'collection_id=' . urlencode( $collectionId )
1171            . '&writer=' . urlencode( $writer )
1172            . '&return_to=' . urlencode( $return_to );
1173
1174        switch ( $result->get( 'state' ) ) {
1175            case 'pending':
1176            case 'progress':
1177                $out->addHeadItem(
1178                    'refresh-nojs',
1179                    '<noscript><meta http-equiv="refresh" content="2" /></noscript>'
1180                );
1181                $out->addInlineScript( 'var collection_id = ' . Html::encodeJsVar( urlencode( $collectionId ) ) . ';' );
1182                $out->addInlineScript( 'var writer = ' . Html::encodeJsVar( urlencode( $writer ) ) . ';' );
1183                $out->addInlineScript( 'var collection_rendering = true;' );
1184                $out->addModules( 'ext.collection' );
1185                $out->setPageTitleMsg( $this->msg( 'coll-rendering_title' ) );
1186
1187                $statusText = $result->get( 'status', 'status' );
1188                if ( $statusText ) {
1189                    if ( $result->get( 'status', 'article' ) ) {
1190                        $statusText .= ' ' . $this->msg(
1191                                'coll-rendering_article',
1192                                $result->get( 'status', 'article' )
1193                            )->text();
1194                    } elseif ( $result->get( 'status', 'page' ) ) {
1195                        $statusText .= ' ';
1196                        $statusText .= $this->msg( 'coll-rendering_page' )
1197                            ->numParams( $result->get( 'status', 'page' ) )->text();
1198                    }
1199                    $status = $this->msg( 'coll-rendering_status', $statusText )->text();
1200                } else {
1201                    $status = '';
1202                }
1203
1204                $template = new CollectionRenderingTemplate();
1205                $template->set( 'status', $status );
1206                $progress = $result->get( 'status', 'progress' );
1207                if ( !$progress ) {
1208                    $progress = 0.00;
1209                }
1210                $template->set( 'progress', $progress );
1211                $out->addTemplate( $template );
1212                $stats->increment( 'collection.renderingpage.pending' );
1213                break;
1214
1215            case 'finished':
1216                $out->setPageTitleMsg( $this->msg( 'coll-rendering_finished_title' ) );
1217
1218                $template = new CollectionFinishedTemplate();
1219                $template->set(
1220                    'download_url',
1221                    wfExpandUrl(
1222                        SkinTemplate::makeSpecialUrl( 'Book', 'bookcmd=download&' . $query ),
1223                        PROTO_CURRENT
1224                    )
1225                );
1226                $template->set( 'is_cached', $request->getVal( 'is_cached' ) );
1227                $template->set( 'writer', $request->getVal( 'writer' ) );
1228                $template->set( 'query', $query );
1229                $template->set( 'return_to', $return_to );
1230                $out->addTemplate( $template );
1231                $stats->increment( 'collection.renderingpage.finished' );
1232                break;
1233
1234            case 'failed':
1235                $out->setPageTitleMsg( $this->msg( 'coll-rendering_failed_title' ) );
1236                $statusText = $result->get( 'status', 'status' );
1237                if ( $statusText ) {
1238                    $status = $this->msg( 'coll-rendering_failed_status', $statusText )->text();
1239                } else {
1240                    $status = '';
1241                }
1242
1243                $template = new CollectionFailedTemplate();
1244                $template->set( 'status', $status );
1245                $template->set( 'query', $query );
1246                $template->set( 'return_to', $return_to );
1247                $out->addTemplate( $template );
1248                $stats->increment( 'collection.renderingpage.failed' );
1249                break;
1250
1251            default:
1252                $stats->increment( 'collection.renderingpage.unknown' );
1253                throw new UnexpectedValueException( __METHOD__ . "(): unknown state '{$result->get( 'state' )}'" );
1254        }
1255    }
1256
1257    private function download() {
1258        global $wgCollectionContentTypeToFilename;
1259
1260        $request = $this->getRequest();
1261        $collectionId = $request->getVal( 'collection_id' );
1262        $writer = $request->getVal( 'writer' );
1263        $api = CollectionRenderingAPI::instance( $writer );
1264
1265        $this->tempfile = tmpfile();
1266        $r = $api->getRenderStatus( $collectionId );
1267
1268        $info = false;
1269        $url = $r->get( 'url' );
1270        if ( $url ) {
1271            $req = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( $url, [], __METHOD__ );
1272            $req->setCallback( function ( $fh, $content ) {
1273                return fwrite( $this->tempfile, $content );
1274            } );
1275            if ( $req->execute()->isOK() ) {
1276                $info = true;
1277            }
1278            $content_type = $r->get( 'content_type' );
1279            $content_length = $r->get( 'content_length' );
1280            $content_disposition = $r->get( 'content_disposition' );
1281        } else {
1282            $info = $api->download( $collectionId );
1283            $content_type = $info->get( 'content_type' );
1284            $content_length = $info->get( 'download_content_length' );
1285            $content_disposition = null;
1286            if ( $info->isError() ) {
1287                $info = false;
1288            }
1289        }
1290        if ( !$info ) {
1291            $this->getOutput()->showErrorPage(
1292                'coll-download_notfound_title',
1293                'coll-download_notfound_text'
1294            );
1295            return;
1296        }
1297        wfResetOutputBuffers();
1298        header( 'Content-Type: ' . $content_type );
1299        header( 'Content-Length: ' . $content_length );
1300        if ( $content_disposition ) {
1301            header( 'Content-Disposition: ' . $content_disposition );
1302        } else {
1303            $mimeType = explode( ';', $content_type )[0];
1304            if ( isset( $wgCollectionContentTypeToFilename[$mimeType] ) ) {
1305                header(
1306                    'Content-Disposition: ' .
1307                    'inline; filename=' .
1308                    $wgCollectionContentTypeToFilename[$mimeType]
1309                );
1310            }
1311        }
1312        fseek( $this->tempfile, 0 );
1313        fpassthru( $this->tempfile );
1314        $this->getOutput()->disable();
1315    }
1316
1317    /**
1318     * Render a single page: fetch page name and revision information, then
1319     * assemble and feed to renderCollection() a single-item $collection.
1320     * @param Title $title Full page name aka prefixed title.
1321     * @param int $oldid
1322     * @return array|null
1323     */
1324    private function makeCollection( $title, $oldid ) {
1325        if ( $title === null ) {
1326            $this->getOutput()->showErrorPage( 'coll-notitle_title', 'coll-notitle_msg' );
1327            return null;
1328        }
1329        $article = [
1330            'type' => 'article',
1331            'content_type' => 'text/x-wiki',
1332            'title' => $title->getPrefixedText()
1333        ];
1334        if ( $oldid ) {
1335            $article['revision'] = (string)$oldid;
1336        }
1337
1338        $revision = MediaWikiServices::getInstance()
1339            ->getRevisionLookup()
1340            ->getRevisionByTitle( $title, $oldid );
1341        if ( $revision ) {
1342            $article['timestamp'] = wfTimestamp( TS_UNIX, $revision->getTimestamp() );
1343        }
1344        return [ 'items' => [ $article ] ];
1345    }
1346
1347    /**
1348     * Apply query string parameters to the given collection.
1349     * Use defaults specified in $wgCollectionRendererSettings.
1350     * @param array &$collection
1351     * @param WebRequest &$request
1352     */
1353    private function applySettings( &$collection, &$request ) {
1354        global $wgCollectionRendererSettings;
1355        if ( !isset( $collection['settings'] ) ) {
1356            $collection['settings'] = [];
1357        }
1358        foreach ( $wgCollectionRendererSettings as $key => $desc ) {
1359            if ( $desc['type'] != 'select' ) {
1360                continue;
1361            }
1362            $val = $request->getVal( $key );
1363            if ( !isset( $collections['settings'][$key] ) ) {
1364                $collection['settings'][$key] = $desc['default'];
1365            }
1366            if ( $val !== null ) {
1367                foreach ( $desc['options'] as $ignore => $valid ) {
1368                    if ( $val == $valid ) {
1369                        $collection['settings'][$key] = $valid;
1370                    }
1371                }
1372            }
1373        }
1374    }
1375
1376    /**
1377     * @param array $collection
1378     * @param string $partner
1379     */
1380    private function postZip( array $collection, $partner ) {
1381        $out = $this->getOutput();
1382        if ( !isset( $this->mPODPartners[$partner] ) ) {
1383            $out->showErrorPage( 'coll-invalid_podpartner_title', 'coll-invalid_podpartner_msg' );
1384            return;
1385        }
1386
1387        $api = CollectionRenderingAPI::instance();
1388        $result = $api->postZip( $collection, $this->mPODPartners[$partner]['posturl'] );
1389        if ( !$this->handleResult( $result ) ) {
1390            return;
1391        }
1392        if ( $result->get( 'redirect_url' ) ) {
1393            $out->redirect( $result->get( 'redirect_url' ) );
1394        }
1395    }
1396
1397    /**
1398     * @param string $colltype
1399     * @param string $title
1400     * @param string $pcollname
1401     * @param string $ccollname
1402     */
1403    private function renderSaveOverwritePage( $colltype, $title, $pcollname, $ccollname ) {
1404        $this->setHeaders();
1405        $out = $this->getOutput();
1406        $out->setPageTitleMsg( $this->msg( 'coll-save_collection' ) );
1407
1408        $template = new CollectionSaveOverwriteTemplate();
1409        $template->set( 'title', $title );
1410        $template->set( 'pcollname', $pcollname );
1411        $template->set( 'ccollname', $ccollname );
1412        $template->set( 'colltype', $colltype );
1413        $template->set( 'skin', $out->getSkin() );
1414        $this->getOutput()->addTemplate( $template );
1415    }
1416
1417    /**
1418     * @param string $title
1419     */
1420    private function renderLoadOverwritePage( $title ) {
1421        $this->setHeaders();
1422        $this->getOutput()->setPageTitleMsg( $this->msg( 'coll-load_collection' ) );
1423
1424        $template = new CollectionLoadOverwriteTemplate();
1425        $template->set( 'output', $this->getOutput() );
1426        $template->set( 'title', $title );
1427        $this->getOutput()->addTemplate( $template );
1428    }
1429
1430    /**
1431     * @param CollectionAPIResult $result
1432     *
1433     * @return bool Whether the result had errors
1434     */
1435    private function handleResult( CollectionAPIResult $result ) {
1436        if ( !$result->isError() ) {
1437            return true;
1438        }
1439
1440        $output = $this->getOutput();
1441        MessageBoxHelper::addModuleStyles( $output );
1442        $output->prepareErrorPage();
1443        $output->setPageTitleMsg( $output->msg( 'coll-request_failed_title' ) );
1444        $output->addHTML( MessageBoxHelper::renderWarningBoxes() );
1445        $output->addWikiMsgArray( 'coll-request_failed_msg', [] );
1446        $output->returnToMain();
1447
1448        return false;
1449    }
1450
1451    protected function getGroupName() {
1452        return 'pagetools';
1453    }
1454}