Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.00% covered (warning)
68.00%
51 / 75
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageArchive
68.00% covered (warning)
68.00%
51 / 75
33.33% covered (danger)
33.33%
2 / 6
28.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listPagesBySearch
50.00% covered (danger)
50.00%
13 / 26
0.00% covered (danger)
0.00%
0 / 1
6.00
 listPagesByPrefix
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 listPages
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 listFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 undeleteAsUser
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
8.16
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\FileRepo\File\FileSelectQueryBuilder;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Page\UndeletePage;
24use MediaWiki\Title\Title;
25use MediaWiki\User\UserIdentity;
26use Wikimedia\Rdbms\IExpression;
27use Wikimedia\Rdbms\IReadableDatabase;
28use Wikimedia\Rdbms\IResultWrapper;
29use Wikimedia\Rdbms\LikeValue;
30use Wikimedia\Rdbms\SelectQueryBuilder;
31
32/**
33 * Used to show archived pages and eventually restore them.
34 */
35class PageArchive {
36
37    protected Title $title;
38
39    /**
40     * @param Title $title
41     */
42    public function __construct( Title $title ) {
43        $this->title = $title;
44    }
45
46    /**
47     * List deleted pages recorded in the archive matching the
48     * given term, using search engine archive.
49     * Returns result wrapper with (ar_namespace, ar_title, count) fields.
50     *
51     * @param string $term Search term
52     * @return IResultWrapper|bool
53     */
54    public static function listPagesBySearch( $term ) {
55        $title = Title::newFromText( $term );
56        if ( $title ) {
57            $ns = $title->getNamespace();
58            $termMain = $title->getText();
59            $termDb = $title->getDBkey();
60        } else {
61            // Prolly won't work too good
62            // @todo handle bare namespace names cleanly?
63            $ns = 0;
64            $termMain = $termDb = $term;
65        }
66
67        // Try search engine first
68        $engine = MediaWikiServices::getInstance()->newSearchEngine();
69        $engine->setLimitOffset( 100 );
70        $engine->setNamespaces( [ $ns ] );
71        $results = $engine->searchArchiveTitle( $termMain );
72        if ( !$results->isOK() ) {
73            $results = [];
74        } else {
75            $results = $results->getValue();
76        }
77
78        if ( !$results ) {
79            // Fall back to regular prefix search
80            return self::listPagesByPrefix( $term );
81        }
82
83        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
84        $condTitles = array_values( array_unique( array_map( static function ( Title $t ) {
85            return $t->getDBkey();
86        }, $results ) ) );
87        $conds = [
88            'ar_namespace' => $ns,
89            $dbr->expr( 'ar_title', '=', $condTitles )
90                ->or( 'ar_title', IExpression::LIKE, new LikeValue( $termDb, $dbr->anyString() ) ),
91        ];
92
93        return self::listPages( $dbr, $conds );
94    }
95
96    /**
97     * List deleted pages recorded in the archive table matching the
98     * given title prefix.
99     * Returns result wrapper with (ar_namespace, ar_title, count) fields.
100     *
101     * @param string $prefix Title prefix
102     * @return IResultWrapper|bool
103     */
104    public static function listPagesByPrefix( $prefix ) {
105        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
106
107        $title = Title::newFromText( $prefix );
108        if ( $title ) {
109            $ns = $title->getNamespace();
110            $prefix = $title->getDBkey();
111        } else {
112            // Prolly won't work too good
113            // @todo handle bare namespace names cleanly?
114            $ns = 0;
115        }
116
117        $conds = [
118            'ar_namespace' => $ns,
119            $dbr->expr( 'ar_title', IExpression::LIKE, new LikeValue( $prefix, $dbr->anyString() ) ),
120        ];
121
122        return self::listPages( $dbr, $conds );
123    }
124
125    /**
126     * @param IReadableDatabase $dbr
127     * @param string|array $condition
128     * @return IResultWrapper
129     */
130    protected static function listPages( IReadableDatabase $dbr, $condition ) {
131        return $dbr->newSelectQueryBuilder()
132            ->select( [ 'ar_namespace', 'ar_title', 'count' => 'COUNT(*)' ] )
133            ->from( 'archive' )
134            ->where( $condition )
135            ->groupBy( [ 'ar_namespace', 'ar_title' ] )
136            ->orderBy( [ 'ar_namespace', 'ar_title' ] )
137            ->limit( 100 )
138            ->caller( __METHOD__ )->fetchResultSet();
139    }
140
141    /**
142     * List the deleted file revisions for this page, if it's a file page.
143     * Returns a result wrapper with various filearchive fields, or null
144     * if not a file page.
145     *
146     * @return IResultWrapper|null
147     * @todo Does this belong in Image for fuller encapsulation?
148     */
149    public function listFiles() {
150        if ( $this->title->getNamespace() !== NS_FILE ) {
151            return null;
152        }
153
154        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
155        $queryBuilder = FileSelectQueryBuilder::newForArchivedFile( $dbr );
156        $queryBuilder->where( [ 'fa_name' => $this->title->getDBkey() ] )
157            ->orderBy( 'fa_timestamp', SelectQueryBuilder::SORT_DESC );
158        return $queryBuilder->caller( __METHOD__ )->fetchResultSet();
159    }
160
161    /**
162     * Restore the given (or all) text and file revisions for the page.
163     * Once restored, the items will be removed from the archive tables.
164     * The deletion log will be updated with an undeletion notice.
165     *
166     * @since 1.35
167     * @deprecated since 1.38, use UndeletePage instead
168     *
169     * @param array $timestamps Pass an empty array to restore all revisions,
170     *   otherwise list the ones to undelete.
171     * @param UserIdentity $user
172     * @param string $comment
173     * @param array $fileVersions
174     * @param bool $unsuppress
175     * @param string|string[]|null $tags Change tags to add to log entry
176     *   ($user should be able to add the specified tags before this is called)
177     * @return array|false [ number of file revisions restored, number of image revisions
178     *   restored, log message ] on success, false on failure.
179     */
180    public function undeleteAsUser(
181        $timestamps,
182        UserIdentity $user,
183        $comment = '',
184        $fileVersions = [],
185        $unsuppress = false,
186        $tags = null
187    ) {
188        $services = MediaWikiServices::getInstance();
189        $page = $services->getWikiPageFactory()->newFromTitle( $this->title );
190        $user = $services->getUserFactory()->newFromUserIdentity( $user );
191        $up = $services->getUndeletePageFactory()->newUndeletePage( $page, $user );
192        if ( is_string( $tags ) ) {
193            $tags = [ $tags ];
194        } elseif ( $tags === null ) {
195            $tags = [];
196        }
197        $status = $up
198            ->setUndeleteOnlyTimestamps( $timestamps )
199            ->setUndeleteOnlyFileVersions( $fileVersions ?: [] )
200            ->setUnsuppress( $unsuppress )
201            ->setTags( $tags ?: [] )
202            ->undeleteUnsafe( $comment );
203        // BC with old return format
204        if ( $status->isGood() ) {
205            $restoredRevs = $status->getValue()[UndeletePage::REVISIONS_RESTORED];
206            $restoredFiles = $status->getValue()[UndeletePage::FILES_RESTORED];
207            if ( $restoredRevs === 0 && $restoredFiles === 0 ) {
208                $ret = false;
209            } else {
210                $ret = [ $restoredRevs, $restoredFiles, $comment ];
211            }
212        } else {
213            $ret = false;
214        }
215        return $ret;
216    }
217
218}