Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageHistoryPseudoPager
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 11
1560
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBody
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
132
 doQuery
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
342
 wrapWithActionButtons
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 preventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPreventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreventClickjacking
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Cache\LinkBatchFactory;
22use MediaWiki\Html\Html;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Pager\ReverseChronologicalPager;
25use MediaWiki\SpecialPage\SpecialPage;
26use MediaWiki\Title\Title;
27use Wikimedia\Timestamp\TimestampException;
28
29class ImageHistoryPseudoPager extends ReverseChronologicalPager {
30    /** @var bool */
31    protected $preventClickjacking = false;
32
33    /**
34     * @var File|null
35     */
36    protected $mImg;
37
38    /**
39     * @var Title
40     */
41    protected $mTitle;
42
43    /**
44     * @since 1.14
45     * @var ImagePage
46     */
47    public $mImagePage;
48
49    /**
50     * @since 1.14
51     * @var File[]
52     */
53    public $mHist;
54
55    /**
56     * @since 1.14
57     * @var int[]
58     */
59    public $mRange;
60
61    /** @var LinkBatchFactory */
62    private $linkBatchFactory;
63
64    /**
65     * @param ImagePage $imagePage
66     * @param LinkBatchFactory|null $linkBatchFactory
67     */
68    public function __construct( $imagePage, ?LinkBatchFactory $linkBatchFactory = null ) {
69        parent::__construct( $imagePage->getContext() );
70        $this->mImagePage = $imagePage;
71        $this->mTitle = $imagePage->getTitle()->createFragmentTarget( 'filehistory' );
72        $this->mImg = null;
73        $this->mHist = [];
74        $this->mRange = [ 0, 0 ]; // display range
75
76        // Only display 10 revisions at once by default, otherwise the list is overwhelming
77        $this->mLimitsShown = array_merge( [ 10 ], $this->mLimitsShown );
78        $this->mDefaultLimit = 10;
79        [ $this->mLimit, /* $offset */ ] =
80            $this->mRequest->getLimitOffsetForUser(
81                $this->getUser(),
82                $this->mDefaultLimit,
83                ''
84            );
85        $this->linkBatchFactory = $linkBatchFactory ?? MediaWikiServices::getInstance()->getLinkBatchFactory();
86    }
87
88    /**
89     * @return Title
90     */
91    public function getTitle() {
92        return $this->mTitle;
93    }
94
95    public function getQueryInfo() {
96        return [];
97    }
98
99    /**
100     * @return string
101     */
102    public function getIndexField() {
103        return '';
104    }
105
106    /**
107     * @param stdClass $row
108     * @return string
109     */
110    public function formatRow( $row ) {
111        return '';
112    }
113
114    /**
115     * @return string
116     */
117    public function getBody() {
118        $s = '';
119        $this->doQuery();
120        if ( count( $this->mHist ) ) {
121            if ( $this->mImg->isLocal() ) {
122                // Do a batch existence check for user pages and talkpages.
123                $linkBatch = $this->linkBatchFactory->newLinkBatch();
124                for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
125                    $file = $this->mHist[$i];
126                    $uploader = $file->getUploader( File::FOR_THIS_USER, $this->getAuthority() );
127                    if ( $uploader ) {
128                        $linkBatch->add( NS_USER, $uploader->getName() );
129                        $linkBatch->add( NS_USER_TALK, $uploader->getName() );
130                    }
131                }
132                $linkBatch->execute();
133            }
134
135            // Batch-format comments
136            $comments = [];
137            for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
138                $file = $this->mHist[$i];
139                $comments[$i] = $file->getDescription(
140                    File::FOR_THIS_USER,
141                    $this->getAuthority()
142                ) ?: '';
143            }
144            $formattedComments = MediaWikiServices::getInstance()
145                ->getCommentFormatter()
146                ->formatStrings( $comments, $this->getTitle() );
147
148            $list = new ImageHistoryList( $this->mImagePage );
149            # Generate prev/next links
150            $navLink = $this->getNavigationBar();
151
152            $s = Html::element( 'h2', [ 'id' => 'filehistory' ], $this->msg( 'filehist' )->text() ) . "\n"
153                . Html::openElement( 'div', [ 'id' => 'mw-imagepage-section-filehistory' ] ) . "\n"
154                . $this->msg( 'filehist-help' )->parseAsBlock()
155                . $navLink . "\n";
156
157            $sList = $list->beginImageHistoryList();
158            $onlyCurrentFile = true;
159            // Skip rows there just for paging links
160            for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
161                $file = $this->mHist[$i];
162                $sList .= $list->imageHistoryLine( !$file->isOld(), $file, $formattedComments[$i] );
163                $onlyCurrentFile = !$file->isOld();
164            }
165            $sList .= $list->endImageHistoryList();
166            if ( $onlyCurrentFile || !$this->mImg->isLocal() ) {
167                // It is not possible to revision-delete the current file or foreign files,
168                // if there is only the current file or the file is not local, show no buttons
169                $s .= $sList;
170            } else {
171                $s .= $this->wrapWithActionButtons( $sList );
172            }
173            $s .= $navLink . "\n" . Html::closeElement( 'div' ) . "\n";
174
175            if ( $list->getPreventClickjacking() ) {
176                $this->setPreventClickjacking( true );
177            }
178        }
179        return $s;
180    }
181
182    public function doQuery() {
183        if ( $this->mQueryDone ) {
184            return;
185        }
186        $this->mImg = $this->mImagePage->getPage()->getFile(); // ensure loading
187        if ( !$this->mImg->exists() ) {
188            return;
189        }
190        // Make sure the date (probably from user input) is valid; if not, drop it.
191        if ( $this->mOffset !== null ) {
192            try {
193                $this->mDb->timestamp( $this->mOffset );
194            } catch ( TimestampException $e ) {
195                $this->mOffset = null;
196            }
197        }
198        $queryLimit = $this->mLimit + 1; // limit plus extra row
199        if ( $this->mIsBackwards ) {
200            // Fetch the file history
201            $this->mHist = $this->mImg->getHistory( $queryLimit, null, $this->mOffset, false );
202            // The current rev may not meet the offset/limit
203            $numRows = count( $this->mHist );
204            if ( $numRows <= $this->mLimit && $this->mImg->getTimestamp() > $this->mOffset ) {
205                $this->mHist = array_merge( [ $this->mImg ], $this->mHist );
206            }
207        } else {
208            // The current rev may not meet the offset
209            if ( !$this->mOffset || $this->mImg->getTimestamp() < $this->mOffset ) {
210                $this->mHist[] = $this->mImg;
211            }
212            // Old image versions (fetch extra row for nav links)
213            $oiLimit = count( $this->mHist ) ? $this->mLimit : $this->mLimit + 1;
214            // Fetch the file history
215            $this->mHist = array_merge( $this->mHist,
216                $this->mImg->getHistory( $oiLimit, $this->mOffset, null, false ) );
217        }
218        $numRows = count( $this->mHist ); // Total number of query results
219        if ( $numRows ) {
220            # Index value of top item in the list
221            $firstIndex = $this->mIsBackwards ?
222                [ $this->mHist[$numRows - 1]->getTimestamp() ] : [ $this->mHist[0]->getTimestamp() ];
223            # Discard the extra result row if there is one
224            if ( $numRows > $this->mLimit && $numRows > 1 ) {
225                if ( $this->mIsBackwards ) {
226                    # Index value of item past the index
227                    $this->mPastTheEndIndex = [ $this->mHist[0]->getTimestamp() ];
228                    # Index value of bottom item in the list
229                    $lastIndex = [ $this->mHist[1]->getTimestamp() ];
230                    # Display range
231                    $this->mRange = [ 1, $numRows - 1 ];
232                } else {
233                    # Index value of item past the index
234                    $this->mPastTheEndIndex = [ $this->mHist[$numRows - 1]->getTimestamp() ];
235                    # Index value of bottom item in the list
236                    $lastIndex = [ $this->mHist[$numRows - 2]->getTimestamp() ];
237                    # Display range
238                    $this->mRange = [ 0, $numRows - 2 ];
239                }
240            } else {
241                # Setting indexes to an empty array means that they will be
242                # omitted if they would otherwise appear in URLs. It just so
243                # happens that this  is the right thing to do in the standard
244                # UI, in all the relevant cases.
245                $this->mPastTheEndIndex = [];
246                # Index value of bottom item in the list
247                $lastIndex = $this->mIsBackwards ?
248                    [ $this->mHist[0]->getTimestamp() ] : [ $this->mHist[$numRows - 1]->getTimestamp() ];
249                # Display range
250                $this->mRange = [ 0, $numRows - 1 ];
251            }
252        } else {
253            $firstIndex = [];
254            $lastIndex = [];
255            $this->mPastTheEndIndex = [];
256        }
257        if ( $this->mIsBackwards ) {
258            $this->mIsFirst = ( $numRows < $queryLimit );
259            $this->mIsLast = ( $this->mOffset == '' );
260            $this->mLastShown = $firstIndex;
261            $this->mFirstShown = $lastIndex;
262        } else {
263            $this->mIsFirst = ( $this->mOffset == '' );
264            $this->mIsLast = ( $numRows < $queryLimit );
265            $this->mLastShown = $lastIndex;
266            $this->mFirstShown = $firstIndex;
267        }
268        $this->mQueryDone = true;
269    }
270
271    /**
272     * Wrap the content with action buttons at begin and end if the user
273     * is allow to use the action buttons.
274     * @param string $formcontents
275     * @return string
276     */
277    private function wrapWithActionButtons( $formcontents ) {
278        if ( !$this->getAuthority()->isAllowed( 'deleterevision' ) ) {
279            return $formcontents;
280        }
281
282        # Show button to hide log entries
283        $s = Html::openElement(
284            'form',
285            [ 'action' => wfScript(), 'id' => 'mw-filehistory-deleterevision-submit' ]
286        ) . "\n";
287        $s .= Html::hidden( 'target', $this->getTitle()->getPrefixedDBkey() ) . "\n";
288        $s .= Html::hidden( 'type', 'oldimage' ) . "\n";
289        $this->setPreventClickjacking( true );
290
291        $buttons = Html::element(
292            'button',
293            [
294                'type' => 'submit',
295                'name' => 'title',
296                'value' => SpecialPage::getTitleFor( 'Revisiondelete' )->getPrefixedDBkey(),
297                'class' => "deleterevision-filehistory-submit mw-filehistory-deleterevision-button mw-ui-button"
298            ],
299            $this->msg( 'showhideselectedfileversions' )->text()
300        ) . "\n";
301
302        $s .= $buttons . $formcontents . $buttons;
303        $s .= Html::closeElement( 'form' );
304
305        return $s;
306    }
307
308    /**
309     * @param bool $enable
310     * @deprecated since 1.38, use ::setPreventClickjacking()
311     */
312    protected function preventClickjacking( $enable = true ) {
313        $this->preventClickjacking = $enable;
314    }
315
316    /**
317     * @param bool $enable
318     * @since 1.38
319     */
320    protected function setPreventClickjacking( bool $enable ) {
321        $this->preventClickjacking = $enable;
322    }
323
324    /**
325     * @return bool
326     */
327    public function getPreventClickjacking() {
328        return $this->preventClickjacking;
329    }
330
331}