DatabaseInstaller::preUpgrade()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * DBMS-specific installation helper.
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 Deployment
22
 */
23
24
/**
25
 * Base class for DBMS-specific installation helper classes.
26
 *
27
 * @ingroup Deployment
28
 * @since 1.17
29
 */
30
abstract class DatabaseInstaller {
31
32
	/**
33
	 * The Installer object.
34
	 *
35
	 * @todo Naming this parent is confusing, 'installer' would be clearer.
36
	 *
37
	 * @var WebInstaller
38
	 */
39
	public $parent;
40
41
	/**
42
	 * The database connection.
43
	 *
44
	 * @var Database
45
	 */
46
	public $db = null;
47
48
	/**
49
	 * Internal variables for installation.
50
	 *
51
	 * @var array
52
	 */
53
	protected $internalDefaults = [];
54
55
	/**
56
	 * Array of MW configuration globals this class uses.
57
	 *
58
	 * @var array
59
	 */
60
	protected $globalNames = [];
61
62
	/**
63
	 * Return the internal name, e.g. 'mysql', or 'sqlite'.
64
	 */
65
	abstract public function getName();
66
67
	/**
68
	 * @return bool Returns true if the client library is compiled in.
69
	 */
70
	abstract public function isCompiled();
71
72
	/**
73
	 * Checks for installation prerequisites other than those checked by isCompiled()
74
	 * @since 1.19
75
	 * @return Status
76
	 */
77
	public function checkPrerequisites() {
78
		return Status::newGood();
79
	}
80
81
	/**
82
	 * Get HTML for a web form that configures this database. Configuration
83
	 * at this time should be the minimum needed to connect and test
84
	 * whether install or upgrade is required.
85
	 *
86
	 * If this is called, $this->parent can be assumed to be a WebInstaller.
87
	 */
88
	abstract public function getConnectForm();
89
90
	/**
91
	 * Set variables based on the request array, assuming it was submitted
92
	 * via the form returned by getConnectForm(). Validate the connection
93
	 * settings by attempting to connect with them.
94
	 *
95
	 * If this is called, $this->parent can be assumed to be a WebInstaller.
96
	 *
97
	 * @return Status
98
	 */
99
	abstract public function submitConnectForm();
100
101
	/**
102
	 * Get HTML for a web form that retrieves settings used for installation.
103
	 * $this->parent can be assumed to be a WebInstaller.
104
	 * If the DB type has no settings beyond those already configured with
105
	 * getConnectForm(), this should return false.
106
	 * @return bool
107
	 */
108
	public function getSettingsForm() {
109
		return false;
110
	}
111
112
	/**
113
	 * Set variables based on the request array, assuming it was submitted via
114
	 * the form return by getSettingsForm().
115
	 *
116
	 * @return Status
117
	 */
118
	public function submitSettingsForm() {
119
		return Status::newGood();
120
	}
121
122
	/**
123
	 * Open a connection to the database using the administrative user/password
124
	 * currently defined in the session, without any caching. Returns a status
125
	 * object. On success, the status object will contain a Database object in
126
	 * its value member.
127
	 *
128
	 * @return Status
129
	 */
130
	abstract public function openConnection();
131
132
	/**
133
	 * Create the database and return a Status object indicating success or
134
	 * failure.
135
	 *
136
	 * @return Status
137
	 */
138
	abstract public function setupDatabase();
139
140
	/**
141
	 * Connect to the database using the administrative user/password currently
142
	 * defined in the session. Returns a status object. On success, the status
143
	 * object will contain a Database object in its value member.
144
	 *
145
	 * This will return a cached connection if one is available.
146
	 *
147
	 * @return Status
148
	 */
149
	public function getConnection() {
150
		if ( $this->db ) {
151
			return Status::newGood( $this->db );
152
		}
153
154
		$status = $this->openConnection();
155
		if ( $status->isOK() ) {
156
			$this->db = $status->value;
157
			// Enable autocommit
158
			$this->db->clearFlag( DBO_TRX );
159
			$this->db->commit( __METHOD__ );
160
		}
161
162
		return $status;
163
	}
164
165
	/**
166
	 * Apply a SQL source file to the database as part of running an installation step.
167
	 *
168
	 * @param string $sourceFileMethod
169
	 * @param string $stepName
170
	 * @param bool $archiveTableMustNotExist
171
	 * @return Status
172
	 */
173
	private function stepApplySourceFile(
174
		$sourceFileMethod,
175
		$stepName,
176
		$archiveTableMustNotExist = false
177
	) {
178
		$status = $this->getConnection();
179
		if ( !$status->isOK() ) {
180
			return $status;
181
		}
182
		$this->db->selectDB( $this->getVar( 'wgDBname' ) );
183
184
		if ( $archiveTableMustNotExist && $this->db->tableExists( 'archive', __METHOD__ ) ) {
185
			$status->warning( "config-$stepName-tables-exist" );
186
			$this->enableLB();
187
188
			return $status;
189
		}
190
191
		$this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
192
		$this->db->begin( __METHOD__ );
193
194
		$error = $this->db->sourceFile(
195
			call_user_func( [ $this, $sourceFileMethod ], $this->db )
196
		);
197
		if ( $error !== true ) {
198
			$this->db->reportQueryError( $error, 0, '', __METHOD__ );
0 ignored issues
show
Bug introduced by
It seems like $error defined by $this->db->sourceFile(ca...ileMethod), $this->db)) on line 194 can also be of type boolean; however, Database::reportQueryError() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
199
			$this->db->rollback( __METHOD__ );
200
			$status->fatal( "config-$stepName-tables-failed", $error );
201
		} else {
202
			$this->db->commit( __METHOD__ );
203
		}
204
		// Resume normal operations
205
		if ( $status->isOK() ) {
206
			$this->enableLB();
207
		}
208
209
		return $status;
210
	}
211
212
	/**
213
	 * Create database tables from scratch.
214
	 *
215
	 * @return Status
216
	 */
217
	public function createTables() {
218
		return $this->stepApplySourceFile( 'getSchemaPath', 'install', true );
219
	}
220
221
	/**
222
	 * Insert update keys into table to prevent running unneded updates.
223
	 *
224
	 * @return Status
225
	 */
226
	public function insertUpdateKeys() {
227
		return $this->stepApplySourceFile( 'getUpdateKeysPath', 'updates', false );
228
	}
229
230
	/**
231
	 * Return a path to the DBMS-specific SQL file if it exists,
232
	 * otherwise default SQL file
233
	 *
234
	 * @param IDatabase $db
235
	 * @param string $filename
236
	 * @return string
237
	 */
238 View Code Duplication
	private function getSqlFilePath( $db, $filename ) {
239
		global $IP;
240
241
		$dbmsSpecificFilePath = "$IP/maintenance/" . $db->getType() . "/$filename";
242
		if ( file_exists( $dbmsSpecificFilePath ) ) {
243
			return $dbmsSpecificFilePath;
244
		} else {
245
			return "$IP/maintenance/$filename";
246
		}
247
	}
248
249
	/**
250
	 * Return a path to the DBMS-specific schema file,
251
	 * otherwise default to tables.sql
252
	 *
253
	 * @param IDatabase $db
254
	 * @return string
255
	 */
256
	public function getSchemaPath( $db ) {
257
		return $this->getSqlFilePath( $db, 'tables.sql' );
258
	}
259
260
	/**
261
	 * Return a path to the DBMS-specific update key file,
262
	 * otherwise default to update-keys.sql
263
	 *
264
	 * @param IDatabase $db
265
	 * @return string
266
	 */
267
	public function getUpdateKeysPath( $db ) {
268
		return $this->getSqlFilePath( $db, 'update-keys.sql' );
269
	}
270
271
	/**
272
	 * Create the tables for each extension the user enabled
273
	 * @return Status
274
	 */
275
	public function createExtensionTables() {
276
		$status = $this->getConnection();
277
		if ( !$status->isOK() ) {
278
			return $status;
279
		}
280
281
		// Now run updates to create tables for old extensions
282
		DatabaseUpdater::newForDB( $this->db )->doUpdates( [ 'extensions' ] );
283
284
		return $status;
285
	}
286
287
	/**
288
	 * Get the DBMS-specific options for LocalSettings.php generation.
289
	 *
290
	 * @return string
291
	 */
292
	abstract public function getLocalSettings();
293
294
	/**
295
	 * Override this to provide DBMS-specific schema variables, to be
296
	 * substituted into tables.sql and other schema files.
297
	 * @return array
298
	 */
299
	public function getSchemaVars() {
300
		return [];
301
	}
302
303
	/**
304
	 * Set appropriate schema variables in the current database connection.
305
	 *
306
	 * This should be called after any request data has been imported, but before
307
	 * any write operations to the database.
308
	 */
309
	public function setupSchemaVars() {
310
		$status = $this->getConnection();
311
		if ( $status->isOK() ) {
312
			$status->value->setSchemaVars( $this->getSchemaVars() );
313
		} else {
314
			$msg = __METHOD__ . ': unexpected error while establishing'
315
				. ' a database connection with message: '
316
				. $status->getMessage()->plain();
317
			throw new MWException( $msg );
318
		}
319
	}
320
321
	/**
322
	 * Set up LBFactory so that wfGetDB() etc. works.
323
	 * We set up a special LBFactory instance which returns the current
324
	 * installer connection.
325
	 */
326
	public function enableLB() {
327
		$status = $this->getConnection();
328
		if ( !$status->isOK() ) {
329
			throw new MWException( __METHOD__ . ': unexpected DB connection error' );
330
		}
331
332
		\MediaWiki\MediaWikiServices::resetGlobalInstance();
333
		$services = \MediaWiki\MediaWikiServices::getInstance();
334
335
		$connection = $status->value;
336
		$services->redefineService( 'DBLoadBalancerFactory', function() use ( $connection ) {
337
			return LBFactorySingle::newFromConnection( $connection );
338
		} );
339
	}
340
341
	/**
342
	 * Perform database upgrades
343
	 *
344
	 * @return bool
345
	 */
346
	public function doUpgrade() {
347
		$this->setupSchemaVars();
348
		$this->enableLB();
349
350
		$ret = true;
351
		ob_start( [ $this, 'outputHandler' ] );
352
		$up = DatabaseUpdater::newForDB( $this->db );
353
		try {
354
			$up->doUpdates();
355
		} catch ( MWException $e ) {
356
			echo "\nAn error occurred:\n";
357
			echo $e->getText();
358
			$ret = false;
359
		} catch ( Exception $e ) {
360
			echo "\nAn error occurred:\n";
361
			echo $e->getMessage();
362
			$ret = false;
363
		}
364
		$up->purgeCache();
365
		ob_end_flush();
366
367
		return $ret;
368
	}
369
370
	/**
371
	 * Allow DB installers a chance to make last-minute changes before installation
372
	 * occurs. This happens before setupDatabase() or createTables() is called, but
373
	 * long after the constructor. Helpful for things like modifying setup steps :)
374
	 */
375
	public function preInstall() {
376
	}
377
378
	/**
379
	 * Allow DB installers a chance to make checks before upgrade.
380
	 */
381
	public function preUpgrade() {
382
	}
383
384
	/**
385
	 * Get an array of MW configuration globals that will be configured by this class.
386
	 * @return array
387
	 */
388
	public function getGlobalNames() {
389
		return $this->globalNames;
390
	}
391
392
	/**
393
	 * Construct and initialise parent.
394
	 * This is typically only called from Installer::getDBInstaller()
395
	 * @param WebInstaller $parent
396
	 */
397
	public function __construct( $parent ) {
398
		$this->parent = $parent;
399
	}
400
401
	/**
402
	 * Convenience function.
403
	 * Check if a named extension is present.
404
	 *
405
	 * @param string $name
406
	 * @return bool
407
	 */
408
	protected static function checkExtension( $name ) {
409
		return extension_loaded( $name );
410
	}
411
412
	/**
413
	 * Get the internationalised name for this DBMS.
414
	 * @return string
415
	 */
416
	public function getReadableName() {
417
		// Messages: config-type-mysql, config-type-postgres, config-type-sqlite,
418
		// config-type-oracle
419
		return wfMessage( 'config-type-' . $this->getName() )->text();
420
	}
421
422
	/**
423
	 * Get a name=>value map of MW configuration globals for the default values.
424
	 * @return array
425
	 */
426
	public function getGlobalDefaults() {
0 ignored issues
show
Coding Style introduced by
getGlobalDefaults uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
427
		$defaults = [];
428
		foreach ( $this->getGlobalNames() as $var ) {
429
			if ( isset( $GLOBALS[$var] ) ) {
430
				$defaults[$var] = $GLOBALS[$var];
431
			}
432
		}
433
		return $defaults;
434
	}
435
436
	/**
437
	 * Get a name=>value map of internal variables used during installation.
438
	 * @return array
439
	 */
440
	public function getInternalDefaults() {
441
		return $this->internalDefaults;
442
	}
443
444
	/**
445
	 * Get a variable, taking local defaults into account.
446
	 * @param string $var
447
	 * @param mixed|null $default
448
	 * @return mixed
449
	 */
450
	public function getVar( $var, $default = null ) {
451
		$defaults = $this->getGlobalDefaults();
452
		$internal = $this->getInternalDefaults();
453
		if ( isset( $defaults[$var] ) ) {
454
			$default = $defaults[$var];
455
		} elseif ( isset( $internal[$var] ) ) {
456
			$default = $internal[$var];
457
		}
458
459
		return $this->parent->getVar( $var, $default );
460
	}
461
462
	/**
463
	 * Convenience alias for $this->parent->setVar()
464
	 * @param string $name
465
	 * @param mixed $value
466
	 */
467
	public function setVar( $name, $value ) {
468
		$this->parent->setVar( $name, $value );
469
	}
470
471
	/**
472
	 * Get a labelled text box to configure a local variable.
473
	 *
474
	 * @param string $var
475
	 * @param string $label
476
	 * @param array $attribs
477
	 * @param string $helpData
478
	 * @return string
479
	 */
480 View Code Duplication
	public function getTextBox( $var, $label, $attribs = [], $helpData = "" ) {
481
		$name = $this->getName() . '_' . $var;
482
		$value = $this->getVar( $var );
483
		if ( !isset( $attribs ) ) {
484
			$attribs = [];
485
		}
486
487
		return $this->parent->getTextBox( [
488
			'var' => $var,
489
			'label' => $label,
490
			'attribs' => $attribs,
491
			'controlName' => $name,
492
			'value' => $value,
493
			'help' => $helpData
494
		] );
495
	}
496
497
	/**
498
	 * Get a labelled password box to configure a local variable.
499
	 * Implements password hiding.
500
	 *
501
	 * @param string $var
502
	 * @param string $label
503
	 * @param array $attribs
504
	 * @param string $helpData
505
	 * @return string
506
	 */
507 View Code Duplication
	public function getPasswordBox( $var, $label, $attribs = [], $helpData = "" ) {
508
		$name = $this->getName() . '_' . $var;
509
		$value = $this->getVar( $var );
510
		if ( !isset( $attribs ) ) {
511
			$attribs = [];
512
		}
513
514
		return $this->parent->getPasswordBox( [
515
			'var' => $var,
516
			'label' => $label,
517
			'attribs' => $attribs,
518
			'controlName' => $name,
519
			'value' => $value,
520
			'help' => $helpData
521
		] );
522
	}
523
524
	/**
525
	 * Get a labelled checkbox to configure a local boolean variable.
526
	 *
527
	 * @param string $var
528
	 * @param string $label
529
	 * @param array $attribs Optional.
530
	 * @param string $helpData Optional.
531
	 * @return string
532
	 */
533 View Code Duplication
	public function getCheckBox( $var, $label, $attribs = [], $helpData = "" ) {
534
		$name = $this->getName() . '_' . $var;
535
		$value = $this->getVar( $var );
536
537
		return $this->parent->getCheckBox( [
538
			'var' => $var,
539
			'label' => $label,
540
			'attribs' => $attribs,
541
			'controlName' => $name,
542
			'value' => $value,
543
			'help' => $helpData
544
		] );
545
	}
546
547
	/**
548
	 * Get a set of labelled radio buttons.
549
	 *
550
	 * @param array $params Parameters are:
551
	 *      var:            The variable to be configured (required)
552
	 *      label:          The message name for the label (required)
553
	 *      itemLabelPrefix: The message name prefix for the item labels (required)
554
	 *      values:         List of allowed values (required)
555
	 *      itemAttribs     Array of attribute arrays, outer key is the value name (optional)
556
	 *
557
	 * @return string
558
	 */
559
	public function getRadioSet( $params ) {
560
		$params['controlName'] = $this->getName() . '_' . $params['var'];
561
		$params['value'] = $this->getVar( $params['var'] );
562
563
		return $this->parent->getRadioSet( $params );
564
	}
565
566
	/**
567
	 * Convenience function to set variables based on form data.
568
	 * Assumes that variables containing "password" in the name are (potentially
569
	 * fake) passwords.
570
	 * @param array $varNames
571
	 * @return array
572
	 */
573
	public function setVarsFromRequest( $varNames ) {
574
		return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' );
575
	}
576
577
	/**
578
	 * Determine whether an existing installation of MediaWiki is present in
579
	 * the configured administrative connection. Returns true if there is
580
	 * such a wiki, false if the database doesn't exist.
581
	 *
582
	 * Traditionally, this is done by testing for the existence of either
583
	 * the revision table or the cur table.
584
	 *
585
	 * @return bool
586
	 */
587
	public function needsUpgrade() {
588
		$status = $this->getConnection();
589
		if ( !$status->isOK() ) {
590
			return false;
591
		}
592
593
		if ( !$this->db->selectDB( $this->getVar( 'wgDBname' ) ) ) {
594
			return false;
595
		}
596
597
		return $this->db->tableExists( 'cur', __METHOD__ ) ||
598
			$this->db->tableExists( 'revision', __METHOD__ );
599
	}
600
601
	/**
602
	 * Get a standard install-user fieldset.
603
	 *
604
	 * @return string
605
	 */
606
	public function getInstallUserBox() {
607
		return Html::openElement( 'fieldset' ) .
608
			Html::element( 'legend', [], wfMessage( 'config-db-install-account' )->text() ) .
609
			$this->getTextBox(
610
				'_InstallUser',
611
				'config-db-username',
612
				[ 'dir' => 'ltr' ],
613
				$this->parent->getHelpBox( 'config-db-install-username' )
614
			) .
615
			$this->getPasswordBox(
616
				'_InstallPassword',
617
				'config-db-password',
618
				[ 'dir' => 'ltr' ],
619
				$this->parent->getHelpBox( 'config-db-install-password' )
620
			) .
621
			Html::closeElement( 'fieldset' );
622
	}
623
624
	/**
625
	 * Submit a standard install user fieldset.
626
	 * @return Status
627
	 */
628
	public function submitInstallUserBox() {
629
		$this->setVarsFromRequest( [ '_InstallUser', '_InstallPassword' ] );
630
631
		return Status::newGood();
632
	}
633
634
	/**
635
	 * Get a standard web-user fieldset
636
	 * @param string|bool $noCreateMsg Message to display instead of the creation checkbox.
637
	 *   Set this to false to show a creation checkbox (default).
638
	 *
639
	 * @return string
640
	 */
641
	public function getWebUserBox( $noCreateMsg = false ) {
642
		$wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : '';
643
		$s = Html::openElement( 'fieldset' ) .
644
			Html::element( 'legend', [], wfMessage( 'config-db-web-account' )->text() ) .
645
			$this->getCheckBox(
646
				'_SameAccount', 'config-db-web-account-same',
647
				[ 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ]
648
			) .
649
			Html::openElement( 'div', [ 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ] ) .
650
			$this->getTextBox( 'wgDBuser', 'config-db-username' ) .
651
			$this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
652
			$this->parent->getHelpBox( 'config-db-web-help' );
653 View Code Duplication
		if ( $noCreateMsg ) {
654
			$s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() );
655
		} else {
656
			$s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
657
		}
658
		$s .= Html::closeElement( 'div' ) . Html::closeElement( 'fieldset' );
659
660
		return $s;
661
	}
662
663
	/**
664
	 * Submit the form from getWebUserBox().
665
	 *
666
	 * @return Status
667
	 */
668
	public function submitWebUserBox() {
669
		$this->setVarsFromRequest(
670
			[ 'wgDBuser', 'wgDBpassword', '_SameAccount', '_CreateDBAccount' ]
671
		);
672
673
		if ( $this->getVar( '_SameAccount' ) ) {
674
			$this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
675
			$this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
676
		}
677
678 View Code Duplication
		if ( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
679
			return Status::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
680
		}
681
682
		return Status::newGood();
683
	}
684
685
	/**
686
	 * Common function for databases that don't understand the MySQLish syntax of interwiki.sql.
687
	 *
688
	 * @return Status
689
	 */
690
	public function populateInterwikiTable() {
691
		$status = $this->getConnection();
692
		if ( !$status->isOK() ) {
693
			return $status;
694
		}
695
		$this->db->selectDB( $this->getVar( 'wgDBname' ) );
696
697
		if ( $this->db->selectRow( 'interwiki', '*', [], __METHOD__ ) ) {
698
			$status->warning( 'config-install-interwiki-exists' );
699
700
			return $status;
701
		}
702
		global $IP;
703
		MediaWiki\suppressWarnings();
704
		$rows = file( "$IP/maintenance/interwiki.list",
705
			FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
706
		MediaWiki\restoreWarnings();
707
		$interwikis = [];
708
		if ( !$rows ) {
709
			return Status::newFatal( 'config-install-interwiki-list' );
710
		}
711
		foreach ( $rows as $row ) {
712
			$row = preg_replace( '/^\s*([^#]*?)\s*(#.*)?$/', '\\1', $row ); // strip comments - whee
713
			if ( $row == "" ) {
714
				continue;
715
			}
716
			$row .= "|";
717
			$interwikis[] = array_combine(
718
				[ 'iw_prefix', 'iw_url', 'iw_local', 'iw_api', 'iw_wikiid' ],
719
				explode( '|', $row )
720
			);
721
		}
722
		$this->db->insert( 'interwiki', $interwikis, __METHOD__ );
723
724
		return Status::newGood();
725
	}
726
727
	public function outputHandler( $string ) {
728
		return htmlspecialchars( $string );
729
	}
730
}
731