Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialDeletedContributions
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 6
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
182
 getSubTitle
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
56
 getForm
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 5
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 * Implements Special:DeletedContributions
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24namespace MediaWiki\Specials;
25
26use LogEventsList;
27use MediaWiki\Block\Block;
28use MediaWiki\Block\DatabaseBlockStore;
29use MediaWiki\Cache\LinkBatchFactory;
30use MediaWiki\CommentFormatter\CommentFormatter;
31use MediaWiki\Html\FormOptions;
32use MediaWiki\HTMLForm\HTMLForm;
33use MediaWiki\MainConfigNames;
34use MediaWiki\Pager\DeletedContribsPager;
35use MediaWiki\Permissions\PermissionManager;
36use MediaWiki\Revision\RevisionFactory;
37use MediaWiki\SpecialPage\SpecialPage;
38use MediaWiki\Title\NamespaceInfo;
39use MediaWiki\User\User;
40use MediaWiki\User\UserFactory;
41use MediaWiki\User\UserNamePrefixSearch;
42use MediaWiki\User\UserNameUtils;
43use MediaWiki\User\UserRigorOptions;
44use Wikimedia\IPUtils;
45use Wikimedia\Rdbms\IConnectionProvider;
46
47/**
48 * Implements Special:DeletedContributions to display archived revisions
49 * @ingroup SpecialPage
50 */
51class SpecialDeletedContributions extends SpecialPage {
52    /** @var FormOptions */
53    protected $mOpts;
54
55    private PermissionManager $permissionManager;
56    private IConnectionProvider $dbProvider;
57    private RevisionFactory $revisionFactory;
58    private NamespaceInfo $namespaceInfo;
59    private UserFactory $userFactory;
60    private UserNameUtils $userNameUtils;
61    private UserNamePrefixSearch $userNamePrefixSearch;
62    private CommentFormatter $commentFormatter;
63    private LinkBatchFactory $linkBatchFactory;
64    private DatabaseBlockStore $blockStore;
65
66    /**
67     * @param PermissionManager $permissionManager
68     * @param IConnectionProvider $dbProvider
69     * @param RevisionFactory $revisionFactory
70     * @param NamespaceInfo $namespaceInfo
71     * @param UserFactory $userFactory
72     * @param UserNameUtils $userNameUtils
73     * @param UserNamePrefixSearch $userNamePrefixSearch
74     * @param CommentFormatter $commentFormatter
75     * @param LinkBatchFactory $linkBatchFactory
76     * @param DatabaseBlockStore $blockStore
77     */
78    public function __construct(
79        PermissionManager $permissionManager,
80        IConnectionProvider $dbProvider,
81        RevisionFactory $revisionFactory,
82        NamespaceInfo $namespaceInfo,
83        UserFactory $userFactory,
84        UserNameUtils $userNameUtils,
85        UserNamePrefixSearch $userNamePrefixSearch,
86        CommentFormatter $commentFormatter,
87        LinkBatchFactory $linkBatchFactory,
88        DatabaseBlockStore $blockStore
89    ) {
90        parent::__construct( 'DeletedContributions', 'deletedhistory' );
91        $this->permissionManager = $permissionManager;
92        $this->dbProvider = $dbProvider;
93        $this->revisionFactory = $revisionFactory;
94        $this->namespaceInfo = $namespaceInfo;
95        $this->userFactory = $userFactory;
96        $this->userNameUtils = $userNameUtils;
97        $this->userNamePrefixSearch = $userNamePrefixSearch;
98        $this->commentFormatter = $commentFormatter;
99        $this->linkBatchFactory = $linkBatchFactory;
100        $this->blockStore = $blockStore;
101    }
102
103    /**
104     * Special page "deleted user contributions".
105     * Shows a list of the deleted contributions of a user.
106     *
107     * @param string|null $par user name of the user for which to show the contributions
108     */
109    public function execute( $par ) {
110        $this->setHeaders();
111        $this->outputHeader();
112        $this->checkPermissions();
113        $out = $this->getOutput();
114        $out->addModuleStyles( [
115            'mediawiki.interface.helpers.styles',
116            'mediawiki.special.changeslist',
117        ] );
118        $this->addHelpLink( 'Help:User contributions' );
119
120        $opts = new FormOptions();
121
122        $opts->add( 'target', '' );
123        $opts->add( 'namespace', '' );
124        $opts->add( 'limit', 20 );
125
126        $opts->fetchValuesFromRequest( $this->getRequest() );
127        $opts->validateIntBounds( 'limit', 0,
128            $this->getConfig()->get( MainConfigNames::QueryPageDefaultLimit ) );
129
130        if ( $par !== null ) {
131            // Beautify the username
132            $par = $this->userNameUtils->getCanonical( $par, UserRigorOptions::RIGOR_NONE );
133            $opts->setValue( 'target', (string)$par );
134        }
135
136        $ns = $opts->getValue( 'namespace' );
137        if ( $ns !== null && $ns !== '' ) {
138            $opts->setValue( 'namespace', intval( $ns ) );
139        }
140
141        $this->mOpts = $opts;
142
143        $target = trim( $opts->getValue( 'target' ) );
144        if ( !strlen( $target ) ) {
145            $this->getForm();
146
147            return;
148        }
149
150        $userObj = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE );
151        if ( !$userObj ) {
152            $this->getForm();
153
154            return;
155        }
156        // Only set valid local user as the relevant user (T344886)
157        // Uses the same condition as the SpecialContributions class did
158        if ( !IPUtils::isValidRange( $target ) &&
159            ( $this->userNameUtils->isIP( $target ) || $userObj->isRegistered() )
160        ) {
161            $this->getSkin()->setRelevantUser( $userObj );
162        }
163
164        $target = $userObj->getName();
165
166        $out->addSubtitle( $this->getSubTitle( $userObj ) );
167        $out->setPageTitleMsg( $this->msg( 'deletedcontributions-title' )->plaintextParams( $target ) );
168
169        $this->getForm();
170
171        $pager = new DeletedContribsPager(
172            $this->getContext(),
173            $this->getHookContainer(),
174            $this->getLinkRenderer(),
175            $this->dbProvider,
176            $this->revisionFactory,
177            $this->commentFormatter,
178            $this->linkBatchFactory,
179            $target,
180            $opts->getValue( 'namespace' )
181        );
182        if ( !$pager->getNumRows() ) {
183            $out->addWikiMsg( 'nocontribs' );
184
185            return;
186        }
187
188        # Show a message about replica DB lag, if applicable
189        $lag = $pager->getDatabase()->getSessionLagStatus()['lag'];
190        if ( $lag > 0 ) {
191            $out->showLagWarning( $lag );
192        }
193
194        $out->addHTML(
195            '<p>' . $pager->getNavigationBar() . '</p>' .
196                $pager->getBody() .
197                '<p>' . $pager->getNavigationBar() . '</p>' );
198
199        # If there were contributions, and it was a valid user or IP, show
200        # the appropriate "footer" message - WHOIS tools, etc.
201        $message = IPUtils::isIPAddress( $target ) ?
202            'sp-contributions-footer-anon' :
203            'sp-contributions-footer';
204
205        if ( !$this->msg( $message )->isDisabled() ) {
206            $out->wrapWikiMsg(
207                "<div class='mw-contributions-footer'>\n$1\n</div>",
208                [ $message, $target ]
209            );
210        }
211    }
212
213    /**
214     * Generates the subheading with links
215     * @param User $userObj User object for the target
216     * @return string Appropriately-escaped HTML to be output literally
217     */
218    private function getSubTitle( $userObj ) {
219        $linkRenderer = $this->getLinkRenderer();
220        if ( $userObj->isAnon() ) {
221            $user = htmlspecialchars( $userObj->getName() );
222        } else {
223            $user = $linkRenderer->makeLink( $userObj->getUserPage(), $userObj->getName() );
224        }
225        $links = '';
226        $nt = $userObj->getUserPage();
227        $talk = $nt->getTalkPage();
228        if ( $talk ) {
229            $tools = SpecialContributions::getUserLinks(
230                $this,
231                $userObj,
232                $this->permissionManager,
233                $this->getHookRunner()
234            );
235
236            $contributionsLink = $linkRenderer->makeKnownLink(
237                SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ),
238                $this->msg( 'sp-deletedcontributions-contribs' )->text()
239            );
240            if ( isset( $tools['deletedcontribs'] ) ) {
241                // Swap out the deletedcontribs link for our contribs one
242                $tools = wfArrayInsertAfter(
243                    $tools, [ 'contribs' => $contributionsLink ], 'deletedcontribs' );
244                unset( $tools['deletedcontribs'] );
245            } else {
246                $tools['contribs'] = $contributionsLink;
247            }
248
249            $links = $this->getLanguage()->pipeList( $tools );
250
251            // Show a note if the user is blocked and display the last block log entry.
252            $block = $this->blockStore->newFromTarget( $userObj, $userObj );
253            if ( $block !== null && $block->getType() != Block::TYPE_AUTO ) {
254                if ( $block->getType() == Block::TYPE_RANGE ) {
255                    $nt = $this->namespaceInfo->getCanonicalName( NS_USER )
256                        . ':' . $block->getTargetName();
257                }
258
259                // LogEventsList::showLogExtract() wants the first parameter by ref
260                $out = $this->getOutput();
261                LogEventsList::showLogExtract(
262                    $out,
263                    'block',
264                    $nt,
265                    '',
266                    [
267                        'lim' => 1,
268                        'showIfEmpty' => false,
269                        'msgKey' => [
270                            'sp-contributions-blocked-notice',
271                            $userObj->getName() # Support GENDER in 'sp-contributions-blocked-notice'
272                        ],
273                        'offset' => '' # don't use $this->getRequest() parameter offset
274                    ]
275                );
276            }
277        }
278
279        return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() );
280    }
281
282    /**
283     * Generates the namespace selector form with hidden attributes.
284     */
285    private function getForm() {
286        $opts = $this->mOpts;
287
288        $formDescriptor = [
289            'target' => [
290                'type' => 'user',
291                'name' => 'target',
292                'label-message' => 'sp-contributions-username',
293                'default' => $opts->getValue( 'target' ),
294                'ipallowed' => true,
295            ],
296
297            'namespace' => [
298                'type' => 'namespaceselect',
299                'name' => 'namespace',
300                'label-message' => 'namespace',
301                'all' => '',
302            ],
303        ];
304
305        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
306            ->setWrapperLegendMsg( 'sp-contributions-search' )
307            ->setSubmitTextMsg( 'sp-contributions-submit' )
308            // prevent setting subpage and 'target' parameter at the same time
309            ->setTitle( $this->getPageTitle() )
310            ->setMethod( 'get' )
311            ->prepareForm()
312            ->displayForm( false );
313    }
314
315    /**
316     * Return an array of subpages beginning with $search that this special page will accept.
317     *
318     * @param string $search Prefix to search for
319     * @param int $limit Maximum number of results to return (usually 10)
320     * @param int $offset Number of results to skip (usually 0)
321     * @return string[] Matching subpages
322     */
323    public function prefixSearchSubpages( $search, $limit, $offset ) {
324        $search = $this->userNameUtils->getCanonical( $search );
325        if ( !$search ) {
326            // No prefix suggestion for invalid user
327            return [];
328        }
329        // Autocomplete subpage as user list - public to allow caching
330        return $this->userNamePrefixSearch
331            ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
332    }
333
334    protected function getGroupName() {
335        return 'users';
336    }
337}
338
339/** @deprecated class alias since 1.41 */
340class_alias( SpecialDeletedContributions::class, 'SpecialDeletedContributions' );