Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMobileEditWatchlist
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 11
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getWatchlistInfo
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getLineHtml
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 execute
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPageOffset
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPagesToDisplay
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getEmptyListHtml
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getNextPage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 executeViewEditWatchlist
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
72
 getViewHtml
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getAssociatedNavigationLinks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\FileRepo\RepoGroup;
4use MediaWiki\HookContainer\HookContainer;
5use MediaWiki\Html\Html;
6use MediaWiki\MainConfigNames;
7use MediaWiki\Registration\ExtensionRegistry;
8use MediaWiki\SpecialPage\SpecialPage;
9use MediaWiki\Specials\SpecialEditWatchlist;
10use MediaWiki\Title\Title;
11use MediaWiki\Watchlist\WatchedItemStoreInterface;
12use MobileFrontend\Hooks\HookRunner;
13use MobileFrontend\Models\MobileCollection;
14use MobileFrontend\Models\MobilePage;
15
16/**
17 * The mobile version of the watchlist editing page.
18 * @deprecated in future this should be the core SpecialEditWatchlist page (T109277)
19 */
20class SpecialMobileEditWatchlist extends SpecialEditWatchlist {
21    private const WATCHLIST_TAB_PATHS = [
22        'Special:Watchlist',
23        'Special:EditWatchlist'
24    ];
25    private const LIMIT = 50;
26
27    /** @var string The name of the title to begin listing the watchlist from */
28    protected readonly string $offsetTitle;
29
30    public function __construct(
31        private readonly HookContainer $hookContainer,
32        private readonly RepoGroup $repoGroup,
33        WatchedItemStoreInterface $watchedItemStore,
34    ) {
35        $req = $this->getRequest();
36        $this->offsetTitle = $req->getVal( 'from', '' );
37        parent::__construct( $watchedItemStore );
38    }
39
40    /**
41     * Get a paged list of titles on a user's watchlist, excluding talk pages,
42     * and return as a two-dimensional array with namespace and title.
43     *
44     * @return array
45     */
46    protected function getWatchlistInfo(): array {
47        $titles = [];
48        $lb = $this->linkBatchFactory->newLinkBatch();
49
50        $this->pager->doQuery();
51        $watchedItems = $this->pager->getOrderedResult();
52
53        foreach ( $watchedItems as $item ) {
54            $titles[$item->wl_namespace][$item->wl_title] = $item->expiry;
55            $lb->add( $item->wl_namespace, $item->wl_title );
56        }
57
58        $lb->execute();
59        return $titles;
60    }
61
62    /**
63     * Gets the HTML fragment for a watched page. The client uses a very different
64     * structure for client-rendered items in PageListItem.hogan.
65     *
66     * @param MobilePage $mp a definition of the page to be rendered.
67     * @return string
68     */
69    protected function getLineHtml( MobilePage $mp ) {
70        $thumb = $mp->getSmallThumbnailHtml( true );
71        $title = $mp->getTitle();
72        if ( !$thumb ) {
73            $thumb = Html::rawElement( 'div', [
74                'class' => 'list-thumb list-thumb-placeholder'
75                ], Html::element( 'span', [
76                    'class' => 'mf-icon-image'
77                ] )
78            );
79        }
80        $timestamp = $mp->getLatestTimestamp();
81        $titleText = $title->getPrefixedText();
82        if ( $timestamp ) {
83            $className = 'title';
84        } else {
85            $className = 'title new';
86        }
87
88        $html =
89            Html::openElement( 'li', [
90                'class' => 'page-summary',
91                'title' => $titleText,
92                'data-id' => $title->getArticleID()
93            ] ) .
94            Html::openElement( 'a', [ 'href' => $title->getLocalURL(), 'class' => $className ] );
95        $html .= $thumb;
96        $html .=
97            Html::element( 'h3', [], $titleText );
98
99        $html .= Html::closeElement( 'a' ) .
100            Html::closeElement( 'li' );
101
102        return $html;
103    }
104
105    /**
106     * The main entry point for the page.
107     *
108     * @param string|null $mode Whether the user is viewing, editing, or clearing their
109     *  watchlist
110     */
111    public function execute( $mode ) {
112        // Anons don't get a watchlist edit
113        $this->requireLogin( 'mobile-frontend-watchlist-purpose' );
114
115        $out = $this->getOutput();
116        $out->addBodyClasses( 'mw-mf-special-page' );
117        parent::execute( $mode );
118        $out->setPageTitleMsg( $this->msg( 'watchlist' ) );
119    }
120
121    /**
122     * Finds the offset of the page given in this->offsetTitle
123     * If doesn't exist returns 0 to show from beginning of array of pages.
124     *
125     * @param array $pages
126     * @return int where the index of the next page to be shown.
127     */
128    private function getPageOffset( $pages ) {
129        // Deal with messiness of mediawiki
130        $pages = array_keys( $pages );
131
132        if ( $this->offsetTitle ) {
133            // PHP is stupid. strict test to avoid issues when page '0' is watched.
134            $offset = array_search( $this->offsetTitle, $pages, true );
135            // Deal with cases where invalid title given
136            if ( $offset === false ) {
137                $offset = 0;
138            }
139        } else {
140            $offset = 0;
141        }
142        return $offset;
143    }
144
145    /**
146     * Create paginated view of entire watchlist
147     *
148     * @param array $pages
149     * @return array of pages that should be displayed in current view
150     */
151    private function getPagesToDisplay( $pages ) {
152        $offset = $this->getPageOffset( $pages );
153        // Get the slice we are going to display and display it
154        return array_slice( $pages, $offset, self::LIMIT, true );
155    }
156
157    /**
158     * Get the HTML needed to show if a user doesn't watch any page, show information
159     * how to watch pages where no pages have been watched.
160     * @return string
161     */
162    private function getEmptyListHtml() {
163        $dir = $this->getLanguage()->isRTL() ? 'rtl' : 'ltr';
164
165        $imgUrl = $this->getConfig()->get( MainConfigNames::ExtensionAssetsPath ) .
166            "/MobileFrontend/images/emptywatchlist-page-actions-$dir.png";
167
168        $msg = Html::element( 'p', [],
169            $this->msg( 'mobile-frontend-watchlist-a-z-empty-howto' )->plain()
170        );
171        $msg .= Html::element( 'img', [
172            'src' => $imgUrl,
173            'alt' => $this->msg( 'mobile-frontend-watchlist-a-z-empty-howto-alt' )->plain(),
174        ] );
175
176        return Html::openElement( 'div', [ 'class' => 'info empty-page' ] ) .
177            $msg .
178            Html::element( 'a',
179                [ 'class' => 'button', 'href' => Title::newMainPage()->getLocalURL() ],
180                $this->msg( 'mobile-frontend-watchlist-back-home' )->plain()
181            ) .
182            Html::closeElement( 'div' );
183    }
184
185    /**
186     * Identify the next page to be shown
187     *
188     * @param array $pages
189     * @return string|bool representing title of next page to show or
190     *  false if there isn't another page to show.
191     */
192    private function getNextPage( $pages ) {
193        $total = count( $pages );
194        $offset = $this->getPageOffset( $pages );
195        $limit = self::LIMIT;
196
197        // Work out if we need a more button and where to start from
198        if ( $total > $offset + $limit ) {
199            $pageKeys = array_keys( $pages );
200            $from = $pageKeys[$offset + $limit];
201        } else {
202            $from = false;
203        }
204        return $from;
205    }
206
207    /**
208     * Renders the view/edit (normal) mode of the watchlist.
209     */
210    protected function executeViewEditWatchlist() {
211        $ns = NS_MAIN;
212        $images = [];
213
214        $watchlist = $this->getWatchlistInfo();
215
216        if ( isset( $watchlist[$ns] ) ) {
217            $allPages = $watchlist[$ns];
218            $from = $this->getNextPage( $allPages );
219            $allPages = $this->getPagesToDisplay( $allPages );
220        } else {
221            $allPages = [];
222            $from = false;
223        }
224
225        // Begin rendering of watchlist.
226        $watchlist = [ $ns => $allPages ];
227        ( new HookRunner( $this->hookContainer ) )
228            ->onSpecialMobileEditWatchlist__images(
229                $this->getContext(), $watchlist, $images
230            );
231
232        // create list of pages
233        $mobilePages = new MobileCollection();
234        $pageKeys = array_keys( $watchlist[$ns] );
235        foreach ( $pageKeys as $dbkey ) {
236            if ( isset( $images[$ns][$dbkey] ) ) {
237                $page = new MobilePage(
238                    Title::makeTitleSafe( $ns, $dbkey ),
239                    $this->repoGroup->findFile( $images[$ns][$dbkey] )
240                );
241            } else {
242                $page = new MobilePage( Title::makeTitleSafe( $ns, $dbkey ) );
243            }
244            $mobilePages->add( $page );
245        }
246
247        if ( $mobilePages->isEmpty() ) {
248            $html = $this->getEmptyListHtml();
249        } else {
250            $html = $this->getViewHtml( $mobilePages );
251        }
252        if ( $from ) {
253            // show more link if there are more items to show
254            $qs = [ 'from' => $from ];
255            $html .= Html::element( 'a',
256                [
257                    'class' => 'mw-mf-watchlist-more',
258                    'href' => SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL( $qs ),
259                ],
260                $this->msg( 'mobile-frontend-watchlist-more' )->text() );
261        }
262        $out = $this->getOutput();
263
264        $config = $out->getConfig();
265        $searchParams = $config->get( 'MFSearchAPIParams' );
266        $mfScriptPath = $config->get( 'MFScriptPath' );
267        $pageProps = $config->get( 'MFQueryPropModules' );
268        // Avoid API warnings and allow integration with optional extensions.
269        if ( $mfScriptPath || ExtensionRegistry::getInstance()->isLoaded( 'PageImages' ) ) {
270            $pageProps[] = 'pageimages';
271            $searchParams = array_merge_recursive( $searchParams, [
272                'piprop' => 'thumbnail',
273                'pithumbsize' => MobilePage::SMALL_IMAGE_WIDTH,
274                'pilimit' => 50,
275            ] );
276        }
277
278        $out->addJSConfigVars( [
279            'wgMFScriptPath' => $mfScriptPath,
280            'wgMFSearchAPIParams' => $searchParams,
281            'wgMFQueryPropModules' => $pageProps,
282        ] );
283        $out->addHTML( $html );
284        $out->addModules( 'mobile.special.watchlist.scripts' );
285    }
286
287    /**
288     * @param MobileCollection $collection Collection of pages to get view for
289     * @return string html representation of collection in watchlist view
290     */
291    protected function getViewHtml( MobileCollection $collection ) {
292        $html = Html::openElement( 'ul', [ 'class' => 'content-unstyled mw-mf-page-list thumbs'
293            . ' page-summary-list mw-mf-watchlist-page-list' ] );
294        foreach ( $collection as $mobilePage ) {
295            $html .= $this->getLineHtml( $mobilePage );
296        }
297        $html .= Html::closeElement( 'ul' );
298        return $html;
299    }
300
301    /**
302     * @inheritDoc
303     */
304    public function getAssociatedNavigationLinks() {
305        return self::WATCHLIST_TAB_PATHS;
306    }
307}