Completed
Branch master (e2eefa)
by
unknown
25:58
created

SpecialMergeHistory::showHistory()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 56
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 41
nc 4
nop 0
dl 0
loc 56
rs 9.7251
c 1
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Implements Special:MergeHistory
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
24
/**
25
 * Special page allowing users with the appropriate permissions to
26
 * merge article histories, with some restrictions
27
 *
28
 * @ingroup SpecialPage
29
 */
30
class SpecialMergeHistory extends SpecialPage {
31
	/** @var FormOptions */
32
	protected $mOpts;
33
34
	/** @var Status */
35
	protected $mStatus;
36
37
	/** @var Title|null */
38
	protected $mTargetObj, $mDestObj;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
39
40
	/** @var int[] */
41
	public $prevId;
42
43
	public function __construct() {
44
		parent::__construct( 'MergeHistory', 'mergehistory' );
45
	}
46
47
	public function doesWrites() {
48
		return true;
49
	}
50
51
	public function execute( $par ) {
52
		$this->useTransactionalTimeLimit();
53
54
		$this->checkPermissions();
55
		$this->checkReadOnly();
56
57
		$this->setHeaders();
58
		$this->outputHeader();
59
60
		$this->addHelpLink( 'Help:Merge history' );
61
62
		$opts = new FormOptions();
63
64
		$opts->add( 'target', '' );
65
		$opts->add( 'dest', '' );
66
		$opts->add( 'target', '' );
67
		$opts->add( 'mergepoint', '' );
68
		$opts->add( 'reason', '' );
69
		$opts->add( 'merge', false );
70
71
		$opts->fetchValuesFromRequest( $this->getRequest() );
72
73
		$target = $opts->getValue( 'target' );
74
		$dest = $opts->getValue( 'dest' );
75
		$targetObj = Title::newFromText( $target );
76
		$destObj = Title::newFromText( $dest );
77
		$status = Status::newGood();
78
79
		$this->mOpts = $opts;
80
		$this->mTargetObj = $targetObj;
81
		$this->mDestObj = $destObj;
82
83
		if ( $opts->getValue( 'merge' ) && $targetObj &&
84
			$destObj && $opts->getValue( 'mergepoint' ) !== '' ) {
85
			$this->merge();
86
87
			return;
88
		}
89
90
		if ( $target === '' && $dest === '' ) {
91
			$this->showMergeForm();
92
93
			return;
94
		}
95
96 View Code Duplication
		if ( !$targetObj instanceof Title ) {
97
			$status->merge( Status::newFatal( 'mergehistory-invalid-source' ) );
98
		} elseif ( !$targetObj->exists() ) {
99
			$status->merge( Status::newFatal( 'mergehistory-no-source',
100
				wfEscapeWikiText( $targetObj->getPrefixedText() )
101
			) );
102
		}
103
104 View Code Duplication
		if ( !$destObj instanceof Title ) {
105
			$status->merge( Status::newFatal( 'mergehistory-invalid-destination' ) );
106
		} elseif ( !$destObj->exists() ) {
107
			$status->merge( Status::newFatal( 'mergehistory-no-destination',
108
				wfEscapeWikiText( $destObj->getPrefixedText() )
109
			) );
110
		}
111
112
		if ( $targetObj && $destObj && $targetObj->equals( $destObj ) ) {
113
			$status->merge( Status::newFatal( 'mergehistory-same-destination' ) );
114
		}
115
116
		$this->mStatus = $status;
117
118
		$this->showMergeForm();
119
120
		if ( $status->isOK() ) {
121
			$this->showHistory();
122
		}
123
	}
124
125
	function showMergeForm() {
126
		$formDescriptor = [
127
			'target' => [
128
				'type' => 'title',
129
				'name' => 'target',
130
				'label-message' => 'mergehistory-from',
131
				'required' => true,
132
			],
133
134
			'dest' => [
135
				'type' => 'title',
136
				'name' => 'dest',
137
				'label-message' => 'mergehistory-into',
138
				'required' => true,
139
			],
140
		];
141
142
		$form = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $form is correct as \HTMLForm::factory('ooui...layForm($this->mStatus) (which targets HTMLForm::displayForm()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Unused Code introduced by
$form is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
143
			->setIntro( $this->msg( 'mergehistory-header' ) )
144
			->setWrapperLegendMsg( 'mergehistory-box' )
145
			->setSubmitTextMsg( 'mergehistory-go' )
146
			->setMethod( 'post' )
147
			->prepareForm()
148
			->displayForm( $this->mStatus );
149
	}
150
151
	private function showHistory() {
152
		# List all stored revisions
153
		$revisions = new MergeHistoryPager(
154
			$this, [], $this->mTargetObj, $this->mDestObj
0 ignored issues
show
Bug introduced by
It seems like $this->mTargetObj can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like $this->mDestObj can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
155
		);
156
		$haveRevisions = $revisions && $revisions->getNumRows() > 0;
157
158
		$out = $this->getOutput();
159
		$header = '<h2 id="mw-mergehistory">' .
160
			$this->msg( 'mergehistory-list' )->escaped() . "</h2>\n";
161
162
		if ( $haveRevisions ) {
163
			$hiddenFields = [
164
				'merge' => true,
165
				'target' => $this->mOpts->getValue( 'target' ),
166
				'dest' => $this->mOpts->getValue( 'dest' ),
167
			];
168
169
			$formDescriptor = [
170
				'reason' => [
171
					'type' => 'text',
172
					'name' => 'reason',
173
					'label-message' => 'mergehistory-reason',
174
				],
175
			];
176
177
			$mergeText = $this->msg( 'mergehistory-merge',
178
				$this->mTargetObj->getPrefixedText(),
179
				$this->mDestObj->getPrefixedText()
180
			)->parse();
181
182
			$history = $header .
183
				$revisions->getNavigationBar() .
184
				'<ul>' .
185
				$revisions->getBody() .
186
				'</ul>' .
187
				$revisions->getNavigationBar();
188
189
			$form = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $form is correct as \HTMLForm::factory('ooui...m()->displayForm(false) (which targets HTMLForm::displayForm()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Unused Code introduced by
$form is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
190
				->addHiddenFields( $hiddenFields )
191
				->setPreText( $mergeText )
192
				->setFooterText( $history )
193
				->setSubmitTextMsg( 'mergehistory-submit' )
194
				->setMethod( 'post' )
195
				->prepareForm()
196
				->displayForm( false );
197
		} else {
198
			$out->addHTML( $header );
199
			$out->addWikiMsg( 'mergehistory-empty' );
200
		}
201
202
		# Show relevant lines from the merge log:
203
		$mergeLogPage = new LogPage( 'merge' );
204
		$out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
205
		LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
0 ignored issues
show
Bug introduced by
It seems like $this->mTargetObj can be null; however, showLogExtract() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
206
	}
207
208
	function formatRevisionRow( $row ) {
209
		$rev = new Revision( $row );
210
211
		$stxt = '';
212
		$last = $this->msg( 'last' )->escaped();
213
214
		$ts = wfTimestamp( TS_MW, $row->rev_timestamp );
215
		$checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mOpts->getValue( 'mergepoint' ) === $ts ) );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by wfTimestamp(TS_MW, $row->rev_timestamp) on line 214 can also be of type false; however, Xml::radio() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
216
217
		$user = $this->getUser();
218
219
		$pageLink = Linker::linkKnown(
220
			$rev->getTitle(),
221
			htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ),
222
			[],
223
			[ 'oldid' => $rev->getId() ]
224
		);
225
		if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
226
			$pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
227
		}
228
229
		# Last link
230
		if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
231
			$last = $this->msg( 'last' )->escaped();
232
		} elseif ( isset( $this->prevId[$row->rev_id] ) ) {
233
			$last = Linker::linkKnown(
234
				$rev->getTitle(),
235
				$this->msg( 'last' )->escaped(),
236
				[],
237
				[
238
					'diff' => $row->rev_id,
239
					'oldid' => $this->prevId[$row->rev_id]
240
				]
241
			);
242
		}
243
244
		$userLink = Linker::revUserTools( $rev );
245
246
		$size = $row->rev_len;
247
		if ( !is_null( $size ) ) {
248
			$stxt = Linker::formatRevisionSize( $size );
249
		}
250
		$comment = Linker::revComment( $rev );
251
252
		return Html::rawElement( 'li', [],
253
			$this->msg( 'mergehistory-revisionrow' )
254
				->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
255
	}
256
257
	/**
258
	 * Actually attempt the history move
259
	 *
260
	 * @todo if all versions of page A are moved to B and then a user
261
	 * tries to do a reverse-merge via the "unmerge" log link, then page
262
	 * A will still be a redirect (as it was after the original merge),
263
	 * though it will have the old revisions back from before (as expected).
264
	 * The user may have to "undo" the redirect manually to finish the "unmerge".
265
	 * Maybe this should delete redirects at the target page of merges?
266
	 *
267
	 * @return bool Success
268
	 */
269
	function merge() {
270
		$opts = $this->mOpts;
271
272
		# Get the titles directly from the IDs, in case the target page params
273
		# were spoofed. The queries are done based on the IDs, so it's best to
274
		# keep it consistent...
275
		$targetObj = $this->mTargetObj;
276
		$destObj = $this->mDestObj;
277
278
		if ( is_null( $targetObj ) || is_null( $destObj ) ||
279
			$targetObj->getArticleID() == $destObj->getArticleID() ) {
280
			return false;
281
		}
282
283
		// MergeHistory object
284
		$mh = new MergeHistory( $targetObj, $destObj, $opts->getValue( 'mergepoint' ) );
285
286
		// Merge!
287
		$mergeStatus = $mh->merge( $this->getUser(), $opts->getValue( 'reason' ) );
288
		if ( !$mergeStatus->isOK() ) {
289
			// Failed merge
290
			$this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
291
			return false;
292
		}
293
294
		$targetLink = Linker::link(
295
			$targetObj,
296
			null,
297
			[],
298
			[ 'redirect' => 'no' ]
299
		);
300
301
		$this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
302
			->rawParams( $targetLink )
303
			->params( $destObj->getPrefixedText() )
304
			->numParams( $mh->getMergedRevisionCount() )
305
		);
306
307
		return true;
308
	}
309
310
	protected function getGroupName() {
311
		return 'pagetools';
312
	}
313
}
314