ImageCleanup::appendTitle()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 35 and the first side effect is on line 28.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * Clean up broken, unparseable upload filenames.
4
 *
5
 * Copyright © 2005-2006 Brion Vibber <[email protected]>
6
 * https://www.mediawiki.org/
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 * @author Brion Vibber <brion at pobox.com>
25
 * @ingroup Maintenance
26
 */
27
28
require_once __DIR__ . '/cleanupTable.inc';
29
30
/**
31
 * Maintenance script to clean up broken, unparseable upload filenames.
32
 *
33
 * @ingroup Maintenance
34
 */
35
class ImageCleanup extends TableCleanup {
36
	protected $defaultParams = [
37
		'table' => 'image',
38
		'conds' => [],
39
		'index' => 'img_name',
40
		'callback' => 'processRow',
41
	];
42
43
	public function __construct() {
44
		parent::__construct();
45
		$this->addDescription( 'Script to clean up broken, unparseable upload filenames' );
46
	}
47
48
	protected function processRow( $row ) {
49
		global $wgContLang;
50
51
		$source = $row->img_name;
52
		if ( $source == '' ) {
53
			// Ye olde empty rows. Just kill them.
54
			$this->killRow( $source );
55
56
			return $this->progress( 1 );
57
		}
58
59
		$cleaned = $source;
60
61
		// About half of old bad image names have percent-codes
62
		$cleaned = rawurldecode( $cleaned );
63
64
		// We also have some HTML entities there
65
		$cleaned = Sanitizer::decodeCharReferences( $cleaned );
66
67
		// Some are old latin-1
68
		$cleaned = $wgContLang->checkTitleEncoding( $cleaned );
69
70
		// Many of remainder look like non-normalized unicode
71
		$cleaned = $wgContLang->normalize( $cleaned );
72
73
		$title = Title::makeTitleSafe( NS_FILE, $cleaned );
74
75
		if ( is_null( $title ) ) {
76
			$this->output( "page $source ($cleaned) is illegal.\n" );
77
			$safe = $this->buildSafeTitle( $cleaned );
78
			if ( $safe === false ) {
79
				return $this->progress( 0 );
80
			}
81
			$this->pokeFile( $source, $safe );
82
83
			return $this->progress( 1 );
84
		}
85
86
		if ( $title->getDBkey() !== $source ) {
87
			$munged = $title->getDBkey();
88
			$this->output( "page $source ($munged) doesn't match self.\n" );
89
			$this->pokeFile( $source, $munged );
90
91
			return $this->progress( 1 );
92
		}
93
94
		return $this->progress( 0 );
95
	}
96
97
	/**
98
	 * @param string $name
99
	 */
100
	private function killRow( $name ) {
101
		if ( $this->dryrun ) {
102
			$this->output( "DRY RUN: would delete bogus row '$name'\n" );
103
		} else {
104
			$this->output( "deleting bogus row '$name'\n" );
105
			$db = $this->getDB( DB_MASTER );
106
			$db->delete( 'image',
107
				[ 'img_name' => $name ],
108
				__METHOD__ );
109
		}
110
	}
111
112
	private function filePath( $name ) {
113
		if ( !isset( $this->repo ) ) {
114
			$this->repo = RepoGroup::singleton()->getLocalRepo();
0 ignored issues
show
Bug introduced by
The property repo does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
115
		}
116
117
		return $this->repo->getRootDirectory() . '/' . $this->repo->getHashPath( $name ) . $name;
118
	}
119
120
	private function imageExists( $name, $db ) {
121
		return $db->selectField( 'image', '1', [ 'img_name' => $name ], __METHOD__ );
122
	}
123
124
	private function pageExists( $name, $db ) {
125
		return $db->selectField(
126
			'page',
127
			'1',
128
			[ 'page_namespace' => NS_FILE, 'page_title' => $name ],
129
			__METHOD__
130
		);
131
	}
132
133
	private function pokeFile( $orig, $new ) {
134
		$path = $this->filePath( $orig );
135
		if ( !file_exists( $path ) ) {
136
			$this->output( "missing file: $path\n" );
137
			$this->killRow( $orig );
138
139
			return;
140
		}
141
142
		$db = $this->getDB( DB_MASTER );
143
144
		/*
145
		 * To prevent key collisions in the update() statements below,
146
		 * if the target title exists in the image table, or if both the
147
		 * original and target titles exist in the page table, append
148
		 * increasing version numbers until the target title exists in
149
		 * neither.  (See also bug 16916.)
150
		 */
151
		$version = 0;
152
		$final = $new;
153
		$conflict = ( $this->imageExists( $final, $db ) ||
154
			( $this->pageExists( $orig, $db ) && $this->pageExists( $final, $db ) ) );
155
156
		while ( $conflict ) {
157
			$this->output( "Rename conflicts with '$final'...\n" );
158
			$version++;
159
			$final = $this->appendTitle( $new, "_$version" );
160
			$conflict = ( $this->imageExists( $final, $db ) || $this->pageExists( $final, $db ) );
161
		}
162
163
		$finalPath = $this->filePath( $final );
164
165
		if ( $this->dryrun ) {
166
			$this->output( "DRY RUN: would rename $path to $finalPath\n" );
167
		} else {
168
			$this->output( "renaming $path to $finalPath\n" );
169
			// @todo FIXME: Should this use File::move()?
170
			$this->beginTransaction( $db, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $db defined by $this->getDB(DB_MASTER) on line 142 can be null; however, Maintenance::beginTransaction() 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...
171
			$db->update( 'image',
172
				[ 'img_name' => $final ],
173
				[ 'img_name' => $orig ],
174
				__METHOD__ );
175
			$db->update( 'oldimage',
176
				[ 'oi_name' => $final ],
177
				[ 'oi_name' => $orig ],
178
				__METHOD__ );
179
			$db->update( 'page',
180
				[ 'page_title' => $final ],
181
				[ 'page_title' => $orig, 'page_namespace' => NS_FILE ],
182
				__METHOD__ );
183
			$dir = dirname( $finalPath );
184
			if ( !file_exists( $dir ) ) {
185
				if ( !wfMkdirParents( $dir, null, __METHOD__ ) ) {
186
					$this->output( "RENAME FAILED, COULD NOT CREATE $dir" );
187
					$this->rollbackTransaction( $db, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $db defined by $this->getDB(DB_MASTER) on line 142 can be null; however, Maintenance::rollbackTransaction() 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...
188
189
					return;
190
				}
191
			}
192
			if ( rename( $path, $finalPath ) ) {
193
				$this->commitTransaction( $db, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $db defined by $this->getDB(DB_MASTER) on line 142 can be null; however, Maintenance::commitTransaction() 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...
194
			} else {
195
				$this->error( "RENAME FAILED" );
196
				$this->rollbackTransaction( $db, __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $db defined by $this->getDB(DB_MASTER) on line 142 can be null; however, Maintenance::rollbackTransaction() 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...
197
			}
198
		}
199
	}
200
201
	private function appendTitle( $name, $suffix ) {
202
		return preg_replace( '/^(.*)(\..*?)$/',
203
			"\\1$suffix\\2", $name );
204
	}
205
206
	private function buildSafeTitle( $name ) {
207
		$x = preg_replace_callback(
208
			'/([^' . Title::legalChars() . ']|~)/',
209
			[ $this, 'hexChar' ],
210
			$name );
211
212
		$test = Title::makeTitleSafe( NS_FILE, $x );
213
		if ( is_null( $test ) || $test->getDBkey() !== $x ) {
214
			$this->error( "Unable to generate safe title from '$name', got '$x'" );
215
216
			return false;
217
		}
218
219
		return $x;
220
	}
221
}
222
223
$maintClass = "ImageCleanup";
224
require_once RUN_MAINTENANCE_IF_MAIN;
225