Completed
Branch master (62f6c6)
by
unknown
21:31
created

ImageHistoryList::imageHistoryLine()   F

Complexity

Conditions 29
Paths > 20000

Size

Total Lines 165
Code Lines 113

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 29
eloc 113
nc 36864
nop 2
dl 0
loc 165
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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
 * 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
21
/**
22
 * Builds the image revision log shown on image pages
23
 *
24
 * @ingroup Media
25
 */
26
class ImageHistoryList extends ContextSource {
27
28
	/**
29
	 * @var Title
30
	 */
31
	protected $title;
32
33
	/**
34
	 * @var File
35
	 */
36
	protected $img;
37
38
	/**
39
	 * @var ImagePage
40
	 */
41
	protected $imagePage;
42
43
	/**
44
	 * @var File
45
	 */
46
	protected $current;
47
48
	protected $repo, $showThumb;
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...
49
	protected $preventClickjacking = false;
50
51
	/**
52
	 * @param ImagePage $imagePage
53
	 */
54
	public function __construct( $imagePage ) {
55
		global $wgShowArchiveThumbnails;
56
		$this->current = $imagePage->getPage()->getFile();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WikiPage as the method getFile() does only exist in the following sub-classes of WikiPage: WikiFilePage. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
57
		$this->img = $imagePage->getDisplayedFile();
58
		$this->title = $imagePage->getTitle();
59
		$this->imagePage = $imagePage;
60
		$this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender();
61
		$this->setContext( $imagePage->getContext() );
62
	}
63
64
	/**
65
	 * @return ImagePage
66
	 */
67
	public function getImagePage() {
68
		return $this->imagePage;
69
	}
70
71
	/**
72
	 * @return File
73
	 */
74
	public function getFile() {
75
		return $this->img;
76
	}
77
78
	/**
79
	 * @param string $navLinks
80
	 * @return string
81
	 */
82
	public function beginImageHistoryList( $navLinks = '' ) {
83
		return Xml::element( 'h2', [ 'id' => 'filehistory' ], $this->msg( 'filehist' )->text() )
84
		. "\n"
85
		. "<div id=\"mw-imagepage-section-filehistory\">\n"
86
		. $this->msg( 'filehist-help' )->parseAsBlock()
87
		. $navLinks . "\n"
88
		. Xml::openElement( 'table', [ 'class' => 'wikitable filehistory' ] ) . "\n"
89
		. '<tr><th></th>'
90
		. ( $this->current->isLocal()
91
		&& ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<th></th>' : '' )
92
		. '<th>' . $this->msg( 'filehist-datetime' )->escaped() . '</th>'
93
		. ( $this->showThumb ? '<th>' . $this->msg( 'filehist-thumb' )->escaped() . '</th>' : '' )
94
		. '<th>' . $this->msg( 'filehist-dimensions' )->escaped() . '</th>'
95
		. '<th>' . $this->msg( 'filehist-user' )->escaped() . '</th>'
96
		. '<th>' . $this->msg( 'filehist-comment' )->escaped() . '</th>'
97
		. "</tr>\n";
98
	}
99
100
	/**
101
	 * @param string $navLinks
102
	 * @return string
103
	 */
104
	public function endImageHistoryList( $navLinks = '' ) {
105
		return "</table>\n$navLinks\n</div>\n";
106
	}
107
108
	/**
109
	 * @param bool $iscur
110
	 * @param File $file
111
	 * @return string
112
	 */
113
	public function imageHistoryLine( $iscur, $file ) {
114
		global $wgContLang;
115
116
		$user = $this->getUser();
117
		$lang = $this->getLanguage();
118
		$timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
119
		$img = $iscur ? $file->getName() : $file->getArchiveName();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class File as the method getArchiveName() does only exist in the following sub-classes of File: OldLocalFile. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
120
		$userId = $file->getUser( 'id' );
121
		$userText = $file->getUser( 'text' );
122
		$description = $file->getDescription( File::FOR_THIS_USER, $user );
123
124
		$local = $this->current->isLocal();
125
		$row = $selected = '';
126
127
		// Deletion link
128
		if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
129
			$row .= '<td>';
130
			# Link to remove from history
131
			if ( $user->isAllowed( 'delete' ) ) {
132
				$q = [ 'action' => 'delete' ];
133
				if ( !$iscur ) {
134
					$q['oldimage'] = $img;
135
				}
136
				$row .= Linker::linkKnown(
137
					$this->title,
138
					$this->msg( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' )->escaped(),
139
					[], $q
140
				);
141
			}
142
			# Link to hide content. Don't show useless link to people who cannot hide revisions.
143
			$canHide = $user->isAllowed( 'deleterevision' );
144
			if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) {
145
				if ( $user->isAllowed( 'delete' ) ) {
146
					$row .= '<br />';
147
				}
148
				// If file is top revision or locked from this user, don't link
149
				if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED, $user ) ) {
150
					$del = Linker::revDeleteLinkDisabled( $canHide );
151
				} else {
152
					list( $ts, ) = explode( '!', $img, 2 );
153
					$query = [
154
						'type' => 'oldimage',
155
						'target' => $this->title->getPrefixedText(),
156
						'ids' => $ts,
157
					];
158
					$del = Linker::revDeleteLink( $query,
159
						$file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
160
				}
161
				$row .= $del;
162
			}
163
			$row .= '</td>';
164
		}
165
166
		// Reversion link/current indicator
167
		$row .= '<td>';
168
		if ( $iscur ) {
169
			$row .= $this->msg( 'filehist-current' )->escaped();
170
		} elseif ( $local && $this->title->quickUserCan( 'edit', $user )
171
			&& $this->title->quickUserCan( 'upload', $user )
172
		) {
173
			if ( $file->isDeleted( File::DELETED_FILE ) ) {
174
				$row .= $this->msg( 'filehist-revert' )->escaped();
175
			} else {
176
				$row .= Linker::linkKnown(
177
					$this->title,
178
					$this->msg( 'filehist-revert' )->escaped(),
179
					[],
180
					[
181
						'action' => 'revert',
182
						'oldimage' => $img,
183
						'wpEditToken' => $user->getEditToken( $img )
184
					]
185
				);
186
			}
187
		}
188
		$row .= '</td>';
189
190
		// Date/time and image link
191
		if ( $file->getTimestamp() === $this->img->getTimestamp() ) {
192
			$selected = "class='filehistory-selected'";
193
		}
194
		$row .= "<td $selected style='white-space: nowrap;'>";
195
		if ( !$file->userCan( File::DELETED_FILE, $user ) ) {
196
			# Don't link to unviewable files
197
			$row .= '<span class="history-deleted">'
198
				. $lang->userTimeAndDate( $timestamp, $user ) . '</span>';
199
		} elseif ( $file->isDeleted( File::DELETED_FILE ) ) {
200
			if ( $local ) {
201
				$this->preventClickjacking();
202
				$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
203
				# Make a link to review the image
204
				$url = Linker::linkKnown(
205
					$revdel,
206
					$lang->userTimeAndDate( $timestamp, $user ),
207
					[],
208
					[
209
						'target' => $this->title->getPrefixedText(),
210
						'file' => $img,
211
						'token' => $user->getEditToken( $img )
212
					]
213
				);
214
			} else {
215
				$url = $lang->userTimeAndDate( $timestamp, $user );
216
			}
217
			$row .= '<span class="history-deleted">' . $url . '</span>';
218
		} elseif ( !$file->exists() ) {
219
			$row .= '<span class="mw-file-missing">'
220
				. $lang->userTimeAndDate( $timestamp, $user ) . '</span>';
221
		} else {
222
			$url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img );
223
			$row .= Xml::element(
224
				'a',
225
				[ 'href' => $url ],
226
				$lang->userTimeAndDate( $timestamp, $user )
227
			);
228
		}
229
		$row .= "</td>";
230
231
		// Thumbnail
232
		if ( $this->showThumb ) {
233
			$row .= '<td>' . $this->getThumbForLine( $file ) . '</td>';
234
		}
235
236
		// Image dimensions + size
237
		$row .= '<td>';
238
		$row .= htmlspecialchars( $file->getDimensionsString() );
239
		$row .= $this->msg( 'word-separator' )->escaped();
240
		$row .= '<span style="white-space: nowrap;">';
241
		$row .= $this->msg( 'parentheses' )->sizeParams( $file->getSize() )->escaped();
242
		$row .= '</span>';
243
		$row .= '</td>';
244
245
		// Uploading user
246
		$row .= '<td>';
247
		// Hide deleted usernames
248
		if ( $file->isDeleted( File::DELETED_USER ) ) {
249
			$row .= '<span class="history-deleted">'
250
				. $this->msg( 'rev-deleted-user' )->escaped() . '</span>';
251
		} else {
252
			if ( $local ) {
253
				$row .= Linker::userLink( $userId, $userText );
254
				$row .= '<span style="white-space: nowrap;">';
255
				$row .= Linker::userToolLinks( $userId, $userText );
256
				$row .= '</span>';
257
			} else {
258
				$row .= htmlspecialchars( $userText );
259
			}
260
		}
261
		$row .= '</td>';
262
263
		// Don't show deleted descriptions
264
		if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
265
			$row .= '<td><span class="history-deleted">' .
266
				$this->msg( 'rev-deleted-comment' )->escaped() . '</span></td>';
267
		} else {
268
			$row .= '<td dir="' . $wgContLang->getDir() . '">' .
269
				Linker::formatComment( $description, $this->title ) . '</td>';
270
		}
271
272
		$rowClass = null;
273
		Hooks::run( 'ImagePageFileHistoryLine', [ $this, $file, &$row, &$rowClass ] );
274
		$classAttr = $rowClass ? " class='$rowClass'" : '';
275
276
		return "<tr{$classAttr}>{$row}</tr>\n";
277
	}
278
279
	/**
280
	 * @param File $file
281
	 * @return string
282
	 */
283
	protected function getThumbForLine( $file ) {
284
		$lang = $this->getLanguage();
285
		$user = $this->getUser();
286
		if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE, $user )
287
			&& !$file->isDeleted( File::DELETED_FILE )
288
		) {
289
			$params = [
290
				'width' => '120',
291
				'height' => '120',
292
			];
293
			$timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
294
295
			$thumbnail = $file->transform( $params );
296
			$options = [
297
				'alt' => $this->msg( 'filehist-thumbtext',
298
					$lang->userTimeAndDate( $timestamp, $user ),
299
					$lang->userDate( $timestamp, $user ),
300
					$lang->userTime( $timestamp, $user ) )->text(),
301
				'file-link' => true,
302
			];
303
304
			if ( !$thumbnail ) {
305
				return $this->msg( 'filehist-nothumb' )->escaped();
306
			}
307
308
			return $thumbnail->toHtml( $options );
309
		} else {
310
			return $this->msg( 'filehist-nothumb' )->escaped();
311
		}
312
	}
313
314
	/**
315
	 * @param bool $enable
316
	 */
317
	protected function preventClickjacking( $enable = true ) {
318
		$this->preventClickjacking = $enable;
319
	}
320
321
	/**
322
	 * @return bool
323
	 */
324
	public function getPreventClickjacking() {
325
		return $this->preventClickjacking;
326
	}
327
}
328