Completed
Push — master ( 14d2bd...06e609 )
by mw
81:37 queued 59:24
created

includes/storage/SMW_SQLHelpers.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Some static helper functions that SMW uses for setting up
5
 * SQL databases.
6
 *
7
 * @author Markus Krötzsch
8
 * @author Marcel Gsteiger
9
 * @author Jeroen De Dauw
10
 *
11
 * @ingroup SMWStore
12
 */
13
class SMWSQLHelpers {
14
15
	/**
16
	 * Database backends often have different types that need to be used
17
	 * repeatedly in (Semantic) MediaWiki. This function provides the
18
	 * preferred type (as a string) for various common kinds of columns.
19
	 * The input is one of the following strings: 'id' (page id numbers or
20
	 * similar), 'title' (title strings or similar), 'namespace' (namespace
21
	 * numbers), 'blob' (longer text blobs), 'iw' (interwiki prefixes).
22
	 *
23
	 * @param string $input
24
	 * @return string|false SQL type declaration
25
	 */
26 3
	static public function getStandardDBType( $input ) {
27 3
		global $wgDBtype;
28
29
		switch ( $input ) {
30 3
			case 'id':
31 3
			return $wgDBtype == 'postgres' ? 'SERIAL' : ($wgDBtype == 'sqlite' ? 'INTEGER' :'INT(8) UNSIGNED'); // like page_id in MW page table
32 3
			case 'namespace':
33 3
			return $wgDBtype == 'postgres' ? 'BIGINT' : 'INT(11)'; // like page_namespace in MW page table
34 3
			case 'title':
35 3
			return $wgDBtype == 'postgres' ? 'TEXT' : 'VARBINARY(255)'; // like page_title in MW page table
36 3
			case 'iw':
37 3
			return ($wgDBtype == 'postgres' || $wgDBtype == 'sqlite') ? 'TEXT' : 'VARBINARY(32)'; // like iw_prefix in MW interwiki table
38 3
			case 'blob':
39 3
			return $wgDBtype == 'postgres' ? 'BYTEA' : 'MEDIUMBLOB'; // larger blobs of character data, usually not subject to SELECT conditions
40
		}
41
42
		return false;
43
	}
44
45
	/**
46
	 * Generic creation and updating function for database tables. Ideally, it
47
	 * would be able to modify a table's signature in arbitrary ways, but it will
48
	 * fail for some changes. Its string-based interface is somewhat too
49
	 * impoverished for a permanent solution. It would be possible to go for update
50
	 * scripts (specific to each change) in the style of MediaWiki instead.
51
	 *
52
	 * Make sure the table of the given name has the given fields, provided
53
	 * as an array with entries fieldname => typeparams. typeparams should be
54
	 * in a normalised form and order to match to existing values.
55
	 *
56
	 * The function returns an array that includes all columns that have been
57
	 * changed. For each such column, the array contains an entry
58
	 * columnname => action, where action is one of 'up', 'new', or 'del'
59
	 *
60
	 * If progress reports during this operation are desired, then the parameter $reportTo should
61
	 * be given an object that has a method reportProgress(string) for doing so.
62
	 *
63
	 * @note The function partly ignores the order in which fields are set up.
64
	 * Only if the type of some field changes will its order be adjusted explicitly.
65
	 *
66
	 * @param string $tableName The table name. Does not need to have been passed to DatabaseBase->tableName yet.
67
	 * @param array $columns The fields and their types the table should have.
68
	 * @param DatabaseBase or Database $db
69
	 * @param object $reportTo Object to report back to.
70
	 */
71 3
	public static function setupTable( $rawTableName, array $fields, $db, $reportTo = null ) {
72 3
		$tableName = $db->tableName( $rawTableName );
73
74 3
		self::reportProgress( "Checking table $tableName ...\n", $reportTo );
75
76 3
		if ( $db->tableExists( $rawTableName ) === false ) { // create new table
77 2
			self::reportProgress( "   Table not found, now creating...\n", $reportTo );
78 2
			self::createTable( $tableName, $fields, $db );
79 2
			self::reportProgress( "   ... done.\n", $reportTo );
0 ignored issues
show
It seems like $reportTo defined by parameter $reportTo on line 71 can also be of type null; however, SMWSQLHelpers::reportProgress() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
80
		} else {
81 2
			self::reportProgress( "   Table already exists, checking structure ...\n", $reportTo );
82 2
			self::updateTable( $tableName, $fields, $db, $reportTo );
83 2
			self::reportProgress( "   ... done.\n", $reportTo );
84
		}
85 3
	}
86
87
	/**
88
	 * Creates a new database table with the specified fields.
89
	 *
90
	 * @param string $tableName The table name.
91
	 * @param array $columns The fields and their types the table should have.
92
	 * @param DatabaseBase|Database $db
93
	 */
94 2
	private static function createTable( $tableName, array $fields, $db ) {
95 2
		global $wgDBtype, $wgDBname;
96
97 2
		$sql = 'CREATE TABLE ' . ( ( $wgDBtype == 'postgres' || $wgDBtype == 'sqlite' ) ? '' : "`$wgDBname`." ) . $tableName . ' (';
98
99 2
		$fieldSql = array();
100
101 2
		foreach ( $fields as $fieldName => $fieldType ) {
102 2
			$fieldSql[] = "$fieldName  $fieldType";
103
		}
104
105 2
		$sql .= implode( ',', $fieldSql ) . ') ';
106
107 2
		if ( $wgDBtype != 'postgres' && $wgDBtype != 'sqlite' ) {
108
			// This replacement is needed for compatibility, see http://bugs.mysql.com/bug.php?id=17501
109 2
			$sql .= str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
110
		}
111
112 2
		$db->query( $sql, __METHOD__ );
113 2
	}
114
115
	/**
116
	 * Update a table given an array of field names and field types.
117
	 *
118
	 * @param string $tableName The table name.
119
	 * @param array $columns The fields and their types the table should have.
120
	 * @param DatabaseBase|Database $db
121
	 * @param object $reportTo Object to report back to.
122
	 */
123 2
	private static function updateTable( $tableName, array $fields, $db, $reportTo ) {
124 2
		global $wgDBtype;
125
126 2
		$currentFields = self::getFields( $tableName, $db, $reportTo );
127
128 2
		$isPostgres = $wgDBtype == 'postgres';
129
130 2
		if ( !$isPostgres ) {
131 2
			$position = 'FIRST';
132
		}
133
134
		// Loop through all the field definitions, and handle each definition for either postgres or MySQL.
135 2
		foreach ( $fields as $fieldName => $fieldType ) {
136 2
			if ( $isPostgres ) {
137
				self::updatePostgresField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo );
138
			} else {
139 2
				self::updateMySqlField( $tableName, $fieldName, $fieldType, $currentFields, $db, $reportTo, $position );
140 2
				$position = "AFTER $fieldName";
141
			}
142
143 2
			$currentFields[$fieldName] = false;
144
		}
145
146
		// The updated fields have their value set to false, so if a field has a value
147
		// that differs from false, it's an obsolete one that should be removed.
148 2
		foreach ( $currentFields as $fieldName => $value ) {
149 2
			if ( $value !== false ) {
150
				self::reportProgress( "   ... deleting obsolete field $fieldName ... ", $reportTo );
151
152
				if ( $isPostgres ) {
153
					$db->query( 'ALTER TABLE "' . $tableName . '" DROP COLUMN "' . $fieldName . '"', __METHOD__ );
154
				} elseif ( $wgDBtype == 'sqlite' ) {
155
					// DROP COLUMN not supported in Sqlite3
156
					self::reportProgress( "   ... deleting obsolete field $fieldName not possible in SQLLite ... you could delete and reinitialize the tables to remove obsolete data, or just keep it ... ", $reportTo );
157
				} else {
158
					$db->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ );
159
				}
160
161 2
				self::reportProgress( "done.\n", $reportTo );
162
			}
163
		}
164 2
	}
165
166
	/**
167
	 * Returns an array of fields (as keys) and their types (as values).
168
	 *
169
	 * @param string $tableName The table name.
170
	 * @param DatabaseBase|Database $db
171
	 * @param object $reportTo to report back to.
172
	 *
173
	 * @return array
174
	 */
175 2
	private static function getFields( $tableName, $db, $reportTo ) {
176 2
		global $wgDBtype;
177
178 2
		if ( $wgDBtype == 'postgres' ) {
179
			$tableName = str_replace( '"', '', $tableName );
180
			// Use the data dictionary in postgresql to get an output comparable to DESCRIBE.
181
			$sql = <<<EOT
182
SELECT
183
	a.attname as "Field",
184
	upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type",
185
	(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
186
	FROM pg_catalog.pg_attrdef d
187
	WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra",
188
		case when a.attnotnull THEN 'NO'::text else 'YES'::text END as "Null", a.attnum
189
	FROM pg_catalog.pg_attribute a
190
	WHERE a.attrelid = (
191
	    SELECT c.oid
192
	    FROM pg_catalog.pg_class c
193
	    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
194
	    WHERE c.relname ~ '^($tableName)$'
195
	    AND pg_catalog.pg_table_is_visible(c.oid)
196
	    LIMIT 1
197
	 ) AND a.attnum > 0 AND NOT a.attisdropped
198
	 ORDER BY a.attnum
199
EOT;
200 2
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
201
			$sql = 'PRAGMA table_info(' . $tableName . ')';
202
		} else { // MySQL
203 2
			$sql = 'DESCRIBE ' . $tableName;
204
		}
205
206 2
		$res = $db->query( $sql, __METHOD__ );
207 2
		$curfields = array();
208
209 2
		foreach ( $res as $row ) {
210 2
			if ( $wgDBtype == 'postgres' ) { // postgresql
211
				$type = strtoupper( $row->Type );
212
213
				if ( preg_match( '/^nextval\\(.+\\)/i', $row->Extra ) ) {
214
					$type = 'SERIAL NOT NULL';
215
				} elseif ( $row->Null != 'YES' ) {
216
						$type .= ' NOT NULL';
217
				}
218 2
			} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
219
				$row->Field = $row->name;
220
				$row->Type = $row->type;
221
				$type = $row->type;
222
				if ( $row->notnull == '1' ) {
223
					$type .= ' NOT NULL';
224
				}
225
				if ( $row->pk == '1' ) {
226
					$type .= ' PRIMARY KEY AUTOINCREMENT';
227
				}
228
			} else { // mysql
229 2
				$type = strtoupper( $row->Type );
230
231 2
				if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) {
232
					$type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us
233
				}
234
235 2
				if ( $row->Null != 'YES' ) {
236 2
					$type .= ' NOT NULL';
237
				}
238
239 2
				if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case.
240 2
					$type .= ' KEY';
241
				}
242
243 2
				if ( $row->Extra == 'auto_increment' ) {
244 2
					$type .= ' AUTO_INCREMENT';
245
				}
246
			}
247
248 2
			$curfields[$row->Field] = $type;
249
		}
250
251 2
		return $curfields;
252
	}
253
254
	/**
255
	 * Update a single field given it's name and type and an array of
256
	 * current fields. Postgres version.
257
	 *
258
	 * @param string $tableName The table name.
259
	 * @param string $name The field name.
260
	 * @param string $type The field type and attributes.
261
	 * @param array $currentFields List of fields as they have been found in the database.
262
	 * @param DatabaseBase|Database $db
263
	 * @param object $reportTo Object to report back to.
264
	 */
265
	private static function updatePostgresField( $tableName, $name, $type, array $currentFields, $db, $reportTo ) {
266
		$keypos = strpos( $type, ' PRIMARY KEY' );
267
268
		if ( $keypos > 0 ) {
269
			$type = substr( $type, 0, $keypos );
270
		}
271
272
		$type = strtoupper( $type );
273
274
		if ( !array_key_exists( $name, $currentFields ) ) {
275
			self::reportProgress( "   ... creating field $name ... ", $reportTo );
276
277
			$db->query( "ALTER TABLE $tableName ADD \"" . $name . "\" $type", __METHOD__ );
278
279
			self::reportProgress( "done.\n", $reportTo );
280
		} elseif ( $currentFields[$name] != $type ) {
281
			self::reportProgress( "   ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo );
282
283
			$notnullposnew = strpos( $type, ' NOT NULL' );
284
			if ( $notnullposnew > 0 ) {
285
				$type = substr( $type, 0, $notnullposnew );
286
			}
287
288
			$notnullposold = strpos( $currentFields[$name], ' NOT NULL' );
289
			$typeold  = strtoupper( ( $notnullposold > 0 ) ? substr( $currentFields[$name], 0, $notnullposold ) : $currentFields[$name] );
290
291
			if ( $typeold != $type ) {
292
				$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $name . "\" TYPE " . $type;
293
				$db->query( $sql, __METHOD__ );
294
			}
295
296
			if ( $notnullposold != $notnullposnew ) {
297
				$sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $name . "\" " . ( $notnullposnew > 0 ? 'SET' : 'DROP' ) . " NOT NULL";
298
				$db->query( $sql, __METHOD__ );
299
			}
300
301
			self::reportProgress( "done.\n", $reportTo );
302
		} else {
303
			self::reportProgress( "   ... field $name is fine.\n", $reportTo );
304
		}
305
	}
306
307
	/**
308
	 * Update a single field given it's name and type and an array of
309
	 * current fields. MySQL version.
310
	 *
311
	 * @param string $tableName The table name.
312
	 * @param string $name The field name.
313
	 * @param string $type The field type and attributes.
314
	 * @param array $currentFields List of fields as they have been found in the database.
315
	 * @param DatabaseBase|Database $db
316
	 * @param object $reportTo Object to report back to.
317
	 * @param string $position
318
	 */
319 2
	private static function updateMySqlField( $tableName, $name, $type, array $currentFields, $db, $reportTo, $position ) {
320 2
		if ( !array_key_exists( $name, $currentFields ) ) {
321
			self::reportProgress( "   ... creating field $name ... ", $reportTo );
322
323
			$db->query( "ALTER TABLE $tableName ADD `$name` $type $position", __METHOD__ );
324
			$result[$name] = 'new';
325
326
			self::reportProgress( "done.\n", $reportTo );
327 2
		} elseif ( $currentFields[$name] != $type ) {
328
			self::reportProgress( "   ... changing type of field $name from '$currentFields[$name]' to '$type' ... ", $reportTo );
329
330
			// To avoid Error: 1068 Multiple primary key defined when a PRIMARY is involved
331
			if ( strpos( $type, 'AUTO_INCREMENT' ) !== false ) {
332
				$db->query( "ALTER TABLE $tableName DROP PRIMARY KEY", __METHOD__ );
333
			}
334
335
			$db->query( "ALTER TABLE $tableName CHANGE `$name` `$name` $type $position", __METHOD__ );
336
			$result[$name] = 'up';
337
			self::reportProgress( "done.\n", $reportTo );
338
		} else {
339 2
			self::reportProgress( "   ... field $name is fine.\n", $reportTo );
340
		}
341 2
	}
342
343
	/**
344
	 * Make sure that each of the column descriptions in the given array is
345
	 * indexed by *one* index in the given DB table.
346
	 *
347
	 * @param string $tableName table name. Does not need to have been passed to DatabaseBase->tableName yet.
348
	 * @param array $indexes array of strings, each a comma separated list with column names to index
349
	 * @param DatabaseBase|Database $db DatabaseBase or Database
350
	 * @param object $reportTo object to report messages to; since 1.8
351
	 */
352 3
	public static function setupIndex( $rawTableName, array $indexes, $db, $reportTo = null ) {
353 3
		global $wgDBtype;
354
355 3
		$tableName = $wgDBtype == 'postgres' ? $db->tableName( $rawTableName, 'raw' ) : $db->tableName( $rawTableName );
356
357 3
		self::reportProgress( "Checking index structures for table $tableName ...\n", $reportTo );
358
359
		// First remove obsolete indexes.
360 3
		$oldIndexes = self::getIndexInfo( $db, $tableName );
361 3
		if ( $wgDBtype == 'sqlite' ) { // SQLite
362
			/// TODO We do not currently get the right column definitions in
363
			/// SQLLite; hence we can only drop all indexes. Wasteful.
364
			foreach ( $oldIndexes as $key => $index ) {
365
				self::dropIndex( $db, $key, $tableName, $key, $reportTo );
366
			}
367
		} else {
368 3
			foreach ( $oldIndexes as $key => $indexColumn ) {
369 3
				$id = array_search( $indexColumn, $indexes );
370 3
				if ( $id !== false || $key == 'PRIMARY' ) {
371 3
					self::reportProgress( "   ... index $indexColumn is fine.\n", $reportTo );
372 3
					unset( $indexes[$id] );
373
				} else { // Duplicate or unrequired index.
374 3
					self::dropIndex( $db, $key, $tableName, $indexColumn, $reportTo );
375
				}
376
			}
377
		}
378
379
		// Add new indexes.
380 3
		foreach ( $indexes as $key => $index ) {
381
			// If the index is an array, it contains the column
382
			// name as first element, and index type as second one.
383 3
			if ( is_array( $index ) ) {
384 3
				$columns = $index[0];
385 3
				$type = count( $index ) > 1 ? $index[1] : 'INDEX';
386
			} else {
387 2
				$columns = $index;
388 2
				$type = 'INDEX';
389
			}
390
391 3
			self::createIndex( $db, $type, "{$tableName}_index{$key}", $tableName, $columns, $reportTo );
392
		}
393
394 3
		self::reportProgress( "   ... done.\n", $reportTo );
395
396 3
		return true;
397
	}
398
399
	/**
400
	 * Get the information about all indexes of a table. The result is an
401
	 * array of format indexname => indexcolumns. The latter is a comma
402
	 * separated list.
403
	 *
404
	 * @since 1.8
405
	 * @param DatabaseBase|Database $db database handler
406
	 * @param string $tableName name of table
407
	 * @return array indexname => columns
408
	 */
409 3
	private static function getIndexInfo( $db, $tableName ) {
410 3
		global $wgDBtype;
411
412 3
		$indexes = array();
413 3
		if ( $wgDBtype == 'postgres' ) { // postgresql
414
			$sql = "SELECT  i.relname AS indexname,"
415
				. " pg_get_indexdef(i.oid) AS indexdef, "
416
				. " replace(substring(pg_get_indexdef(i.oid) from E'\\\\((.*)\\\\)'), ' ' , '') AS indexcolumns"
417
				. " FROM pg_index x"
418
				. " JOIN pg_class c ON c.oid = x.indrelid"
419
				. " JOIN pg_class i ON i.oid = x.indexrelid"
420
				. " LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
421
				. " LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace"
422
				. " WHERE c.relkind = 'r'::\"char\" AND i.relkind = 'i'::\"char\""
423
				. " AND c.relname = '" . $tableName . "'"
424
				. " AND NOT pg_get_indexdef(i.oid) ~ '^CREATE UNIQUE INDEX'";
425
			$res = $db->query( $sql, __METHOD__ );
426
427
			if ( !$res ) {
428
				return false;
429
			}
430
431
			foreach ( $res as $row ) {
432
				$indexes[$row->indexname] = $row->indexcolumns;
433
			}
434 3
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
435
			$res = $db->query( 'PRAGMA index_list(' . $tableName . ')', __METHOD__ );
436
437
			if ( !$res ) {
438
				return false;
439
			}
440
441
			foreach ( $res as $row ) {
442
				/// FIXME The value should not be $row->name below?!
443
				if ( !array_key_exists( $row->name, $indexes ) ) {
444
					$indexes[$row->name] = $row->name;
445
				} else {
446
					$indexes[$row->name] .= ',' . $row->name;
447
				}
448
			}
449
		} else { // MySQL and default
450 3
			$res = $db->query( 'SHOW INDEX FROM ' . $tableName, __METHOD__ );
451
452 3
			if ( !$res ) {
453
				return false;
454
			}
455
456 3
			foreach ( $res as $row ) {
457 3
				if ( !array_key_exists( $row->Key_name, $indexes ) ) {
458 3
					$indexes[$row->Key_name] = $row->Column_name;
459
				} else {
460 3
					$indexes[$row->Key_name] .= ',' . $row->Column_name;
461
				}
462
			}
463
		}
464
465 3
		return $indexes;
466
	}
467
468
	/**
469
	 * Drop an index using the suitable SQL for various RDBMS.
470
	 *
471
	 * @since 1.8
472
	 * @param DatabaseBase|Database $db database handler
473
	 * @param string $indexName name fo the index as in DB
474
	 * @param string $tableName name of the table (not relevant in all DBMSs)
475
	 * @param string $columns list of column names to index, comma
476
	 * separated; only for reporting
477
	 * @param object $reportTo to report messages to
478
	 */
479 2
	private static function dropIndex( $db, $indexName, $tableName, $columns, $reportTo = null ) {
480 2
		global $wgDBtype;
481
482 2
		self::reportProgress( "   ... removing index $columns ...", $reportTo );
483 2
		if ( $wgDBtype == 'postgres' ) { // postgresql
484
			$db->query( 'DROP INDEX IF EXISTS ' . $indexName, __METHOD__ );
485 2
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
486
			$db->query( 'DROP INDEX ' . $indexName, __METHOD__ );
487
		} else { // MySQL and default
488 2
			$db->query( 'DROP INDEX ' . $indexName . ' ON ' . $tableName, __METHOD__ );
489
		}
490 2
		self::reportProgress( "done.\n", $reportTo );
491 2
	}
492
493
	/**
494
	 * Create an index using the suitable SQL for various RDBMS.
495
	 *
496
	 * @since 1.8
497
	 * @param DatabaseBase|Database $db Database handler
498
	 * @param string $type "INDEX", "UNIQUE" or similar
499
	 * @param string $indexName name fo the index as in DB
500
	 * @param string $tableName name of the table
501
	 * @param array $columns list of column names to index, comma separated
502
	 * @param object $reportTo object to report messages to
503
	 */
504 3
	private static function createIndex( $db, $type, $indexName, $tableName, $columns, $reportTo = null ) {
505 3
		global $wgDBtype;
506
507 3
		self::reportProgress( "   ... creating new index $columns ...", $reportTo );
508 3
		if ( $wgDBtype == 'postgres' ) { // postgresql
509
			if ( $db->indexInfo( $tableName, $indexName ) === false ) {
510
				$db->query( "CREATE $type $indexName ON $tableName ($columns)", __METHOD__ );
511
			}
512 3
		} elseif ( $wgDBtype == 'sqlite' ) { // SQLite
513
			$db->query( "CREATE $type $indexName ON $tableName ($columns)", __METHOD__ );
514
		} else { // MySQL and default
515 3
			$db->query( "ALTER TABLE $tableName ADD $type ($columns)", __METHOD__ );
516
		}
517 3
		self::reportProgress( "done.\n", $reportTo );
518 3
	}
519
520
	/**
521
	 * Reports the given message to the reportProgress method of the
522
	 * $receiver.
523
	 *
524
	 * @param string $msg
525
	 * @param object $receiver
526
	 */
527 3
	private static function reportProgress( $msg, $receiver ) {
528 3
		if ( !is_null( $receiver ) ) {
529
			$receiver->reportProgress( $msg );
530
		}
531 3
	}
532
533
}
534