Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsletterTablePager
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 14
2256
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
 getFieldNames
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getSubscribedQuery
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getSecondaryOrderBy
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getUnsubscribedQuery
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getOp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 reallyDoQuery
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
306
 preprocessResults
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
12
 formatValue
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 getCellAttrs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSort
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFieldSortable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUserOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Newsletter\Specials\Pagers;
4
5use IContextSource;
6use MediaWiki\Extension\Newsletter\Newsletter;
7use MediaWiki\Extension\Newsletter\Specials\SpecialNewsletter;
8use MediaWiki\Html\Html;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Pager\TablePager;
11use MediaWiki\SpecialPage\SpecialPage;
12use MediaWiki\Title\Title;
13use Wikimedia\Rdbms\IDatabase;
14use Wikimedia\Rdbms\SelectQueryBuilder;
15
16/**
17 * @license GPL-2.0-or-later
18 * @author Tina Johnson
19 * @author Brian Wolff <bawolff+wn@gmail.com>
20 * @author Tony Thomas <01tonythomas@gmail.com>
21 */
22class NewsletterTablePager extends TablePager {
23
24    /** Added to offset for sorting reasons */
25    private const EXTRAINT = 150000000;
26
27    /**
28     * @var (string|null)[]
29     */
30    private $fieldNames;
31
32    /**
33     * @var string
34     */
35    private $option;
36
37    /** @var string */
38    protected $mode;
39
40    /** @var Newsletter[] */
41    private $newslettersArray;
42
43    public function __construct( IContextSource $context = null, IDatabase $readDb = null ) {
44        if ( $readDb !== null ) {
45            $this->mDb = $readDb;
46        }
47        // Because we mIndexField is not unique
48        // we need the last one.
49        $this->setIncludeOffset( true );
50        parent::__construct( $context );
51    }
52
53    public function getFieldNames() {
54        if ( $this->fieldNames === null ) {
55            $this->fieldNames = [
56                'nl_name' => $this->msg( 'newsletter-header-name' )->text(),
57                'nl_desc' => $this->msg( 'newsletter-header-description' )->text(),
58                'subscriber_count' => $this->msg( 'newsletter-header-subscriber_count' )->text(),
59            ];
60
61            if ( $this->getUser()->isRegistered() ) {
62                // Only logged-in users can (un)subscribe
63                $this->fieldNames['action'] = null;
64            }
65        }
66
67        return $this->fieldNames;
68    }
69
70    /**
71     * Get the query for newsletters for which the user is subscribed to.
72     *
73     * This is either run directly or as part as a union. It's done as part of
74     * a union to avoid expensive filesort.
75     *
76     * @param int $offset The indexpager offset (Number of subscribers)
77     * @param int $limit
78     * @param bool $descending Ascending or descending?
79     * @param string|false $secondaryOffset For tiebreaking the order (nl_name)
80     *
81     * @return SelectQueryBuilder
82     */
83    private function getSubscribedQuery( $offset, $limit, $descending, $secondaryOffset ) {
84        // XXX Hacky
85        $oldIndex = $this->mIndexField;
86        $this->mIndexField = 'nl_subscriber_count';
87        $this->mode = 'subscribed';
88        [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
89            $this->buildQueryInfo( $offset, $limit, $descending );
90
91        if ( $secondaryOffset !== false ) {
92            $conds[] = $this->getSecondaryOrderBy( $descending, $offset, $secondaryOffset );
93        }
94        if ( !$this->mDb->unionSupportsOrderAndLimit() ) {
95            // Sqlite is going to be inefficient
96            unset( $options['ORDER BY'] );
97            unset( $options['LIMIT'] );
98        }
99        $subscribedPart = $this->mDb->newSelectQueryBuilder()
100            ->tables( $tables )
101            ->fields( $fields )
102            ->where( $conds )
103            ->caller( $fname )
104            ->options( $options )
105            ->joinConds( $join_conds );
106        $this->mIndexField = $oldIndex;
107        return $subscribedPart;
108    }
109
110    /**
111     * Add paging conditions for tie-breaking
112     *
113     * @param bool $desc
114     * @param int $offset
115     * @param string|false $secondaryOffset
116     * @return string raw sql
117     */
118    private function getSecondaryOrderBy( $desc, $offset, $secondaryOffset ) {
119        return $this->mDb->buildComparison(
120            $this->getOp( $desc ),
121            [
122                'nl_subscriber_count' => $offset,
123                'nl_name' => $secondaryOffset,
124            ]
125        );
126    }
127
128    /**
129     * Get the query for newsletters for which the user is not subscribed to.
130     *
131     * This is either run directly or as part as a union. its
132     * done as part of a union to avoid expensive filesort.
133     *
134     * @param int $offset The indexpager offset (Number of subscribers)
135     * @param int $limit
136     * @param bool $descending Ascending or descending?
137     * @param string|false $secondaryOffset For tiebreaking the order (nl_name)
138     * @return SelectQueryBuilder
139     */
140    private function getUnsubscribedQuery( $offset, $limit, $descending, $secondaryOffset ) {
141        // XXX Hacky
142        $oldIndex = $this->mIndexField;
143        $this->mIndexField = 'nl_subscriber_count';
144        $this->mode = 'unsubscribed';
145        [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
146            $this->buildQueryInfo( $offset, $limit, $descending );
147        if ( $secondaryOffset !== false ) {
148            $conds[] = $this->getSecondaryOrderBy( $descending, $offset, $secondaryOffset );
149        }
150        if ( !$this->mDb->unionSupportsOrderAndLimit() ) {
151            // Sqlite is going to be inefficient
152            unset( $options['ORDER BY'] );
153            unset( $options['LIMIT'] );
154        }
155        $unsubscribedPart = $this->mDb->newSelectQueryBuilder()
156            ->tables( $tables )
157            ->fields( $fields )
158            ->where( $conds )
159            ->caller( $fname )
160            ->options( $options )
161            ->joinConds( $join_conds );
162        $this->mIndexField = $oldIndex;
163        return $unsubscribedPart;
164    }
165
166    /**
167     * Operator for paging.
168     *
169     * @param bool $desc Descending vs Ascending.
170     * @return string
171     */
172    private function getOp( $desc ) {
173        if ( $desc ) {
174            return '>';
175        } else {
176            return '<';
177        }
178    }
179
180    /**
181     * Hacky stuff with offset in order to actually use two separate queries unioned, sorted on
182     * multiple fields, instead of one query like IndexPager expects.
183     *
184     * @param string $offset
185     * @param int $limit
186     * @param bool $descending
187     * @return mixed
188     */
189    public function reallyDoQuery( $offset, $limit, $descending ) {
190        $pipePos = strpos( $offset, '|' );
191        if ( $pipePos !== false ) {
192            $realOffset = (int)substr( $offset, 1, $pipePos - 1 ) - self::EXTRAINT;
193            $secondaryOffset = substr( $offset, $pipePos + 1 );
194        } elseif ( strlen( $offset ) >= 2 ) {
195            $realOffset = (int)substr( $offset, 1 ) - self::EXTRAINT;
196            $secondaryOffset = false;
197        } else {
198            $realOffset = 0;
199            $secondaryOffset = false;
200        }
201        $offsetMode = $offset === '' ? '' : substr( $offset, 0, 1 );
202        if ( $this->option == 'all' && (
203                ( $offsetMode === '' ) ||
204                ( $offsetMode === 'U' && !$descending ) ||
205                ( $offsetMode === 'S' && $descending )
206            ) ) {
207            $subscribedPart = $this->getSubscribedQuery(
208                $descending ? $realOffset : 0,
209                $limit,
210                $descending,
211                $descending ? $secondaryOffset : false
212            );
213            $unsubscribedPart = $this->getUnsubscribedQuery(
214                $descending ? 0 : $realOffset,
215                $limit,
216                $descending,
217                $descending ? false : $secondaryOffset
218            );
219            $unionQueryBuilder = $this->mDb->newUnionQueryBuilder()
220                ->add( $subscribedPart )->add( $unsubscribedPart )
221                ->all();
222            // For some reason, this is the opposite of what
223            // you would expect.
224            $dir = $descending ? 'ASC' : 'DESC';
225            return $unionQueryBuilder->orderBy( 'sort', $dir )
226                ->limit( (int)$limit )
227                ->caller( __METHOD__ )
228                ->fetchResultSet();
229        } elseif ( $this->option === 'subscribed' || $offsetMode === 'S' ) {
230            return $this->getSubscribedQuery(
231                    $realOffset, $limit, $descending, $secondaryOffset
232                )->fetchResultSet();
233
234        } else {
235            // unsubscribed, or we are out of subscribed results.
236            return $this->getUnsubscribedQuery(
237                    $realOffset, $limit, $descending, $secondaryOffset
238                )->fetchResultSet();
239        }
240    }
241
242    /**
243     * @param \Wikimedia\Rdbms\IResultWrapper $result
244     */
245    public function preprocessResults( $result ) {
246        foreach ( $result as $res ) {
247            $this->newslettersArray[$res->nl_id] = Newsletter::newFromID( (int)$res->nl_id );
248        }
249        parent::preprocessResults( $result );
250    }
251
252    /**
253     * @return array
254     */
255    public function getQueryInfo() {
256        $userId = $this->getUser()->getId();
257        $info = [
258            'tables' => [ 'nl_newsletters', 'nl_subscriptions' ],
259            'fields' => [
260                'nl_name',
261                'nl_desc',
262                'nl_id',
263                'nl_subscriber_count',
264                'nls_subscriber_id'
265            ],
266        ];
267        $info['conds'] = [ 'nl_active' => 1 ];
268
269        if ( $this->mode == "unsubscribed" ) {
270            $info['fields']['sort'] = $this->mDb->buildConcat( [
271                $this->mDb->addQuotes( 'U' ),
272                'nl_subscriber_count+' . self::EXTRAINT,
273                $this->mDb->addQuotes( '|' ),
274                'nl_name',
275            ] );
276            $info['join_conds'] = [
277                'nl_subscriptions' => [
278                    'LEFT OUTER JOIN',
279                    [
280                        'nl_id=nls_newsletter_id',
281                        'nls_subscriber_id' => $userId
282                    ]
283                ]
284            ];
285            $info['conds']['nls_subscriber_id'] = null;
286        } elseif ( $this->mode == "subscribed" ) {
287            $info['fields']['sort'] = $this->mDb->buildConcat( [
288                $this->mDb->addQuotes( 'S' ),
289                'nl_subscriber_count+' . self::EXTRAINT,
290                $this->mDb->addQuotes( '|' ),
291                'nl_name',
292            ] );
293            $info['join_conds'] = [
294                'nl_subscriptions' => [
295                    'INNER JOIN',
296                    [
297                        'nl_id=nls_newsletter_id',
298                        'nls_subscriber_id' => $userId
299                    ]
300                ]
301            ];
302        }
303
304        return $info;
305    }
306
307    public function formatValue( $field, $value ) {
308        $services = MediaWikiServices::getInstance();
309        $linkRenderer = $services->getLinkRenderer();
310        $contLang = $services->getContentLanguage();
311        $id = $this->mCurrentRow->nl_id;
312        $newsletter = $this->newslettersArray[(int)$id];
313        switch ( $field ) {
314            case 'nl_name':
315                $title = Title::makeTitleSafe( NS_NEWSLETTER, $newsletter->getName() );
316                if ( $title ) {
317                    return $linkRenderer->makeKnownLink( $title, $value );
318                } else {
319                    return htmlspecialchars( $value );
320                }
321            case 'nl_desc':
322                return htmlspecialchars( $contLang->truncateForVisual( $value, 644 ) );
323            case 'subscriber_count':
324                return Html::element(
325                    'span',
326                    [ 'id' => "nl-count-$id" ],
327                    $contLang->formatNum( -(int)$this->mCurrentRow->nl_subscriber_count )
328                );
329            case 'action':
330                if ( $this->mCurrentRow->nls_subscriber_id ) {
331                    $title = SpecialPage::getTitleFor(
332                        'Newsletter', $id . '/' . SpecialNewsletter::NEWSLETTER_UNSUBSCRIBE
333                    );
334                    $link = $linkRenderer->makeKnownLink( $title,
335                        $this->msg( 'newsletter-unsubscribe-button' )->text(),
336                        [
337                            'class' => 'newsletter-subscription newsletter-subscribed',
338                            'data-mw-newsletter-id' => $id
339                        ]
340                    );
341                } else {
342                    $title = SpecialPage::getTitleFor(
343                        'Newsletter', $id . '/' . SpecialNewsletter::NEWSLETTER_SUBSCRIBE
344                    );
345                    $link = $linkRenderer->makeKnownLink(
346                        $title,
347                        $this->msg( 'newsletter-subscribe-button' )->text(),
348                        [
349                            'class' => 'newsletter-subscription newsletter-unsubscribed',
350                            'data-mw-newsletter-id' => $id
351                        ]
352                    );
353                }
354
355                return $link;
356        }
357    }
358
359    /**
360     * @param string $field
361     * @param mixed $value
362     * @return array
363     */
364    public function getCellAttrs( $field, $value ) {
365        return parent::getCellAttrs( $field, $value );
366    }
367
368    public function getDefaultSort() {
369        return 'sort';
370    }
371
372    public function isFieldSortable( $field ) {
373        return false;
374    }
375
376    public function setUserOption( $value ) {
377        $this->option = $value;
378    }
379
380}