Completed
Branch master (174b3a)
by
unknown
26:51
created

DatabaseSqlite::requiresDatabaseUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This is the SQLite database abstraction layer.
4
 * See maintenance/sqlite/README for development notes and other specific information
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along
17
 * with this program; if not, write to the Free Software Foundation, Inc.,
18
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 * http://www.gnu.org/copyleft/gpl.html
20
 *
21
 * @file
22
 * @ingroup Database
23
 */
24
25
/**
26
 * @ingroup Database
27
 */
28
class DatabaseSqlite extends DatabaseBase {
29
	/** @var bool Whether full text is enabled */
30
	private static $fulltextEnabled = null;
31
32
	/** @var string Directory */
33
	protected $dbDir;
34
35
	/** @var string File name for SQLite database file */
36
	protected $dbPath;
37
38
	/** @var string Transaction mode */
39
	protected $trxMode;
40
41
	/** @var int The number of rows affected as an integer */
42
	protected $mAffectedRows;
43
44
	/** @var resource */
45
	protected $mLastResult;
46
47
	/** @var PDO */
48
	protected $mConn;
49
50
	/** @var FSLockManager (hopefully on the same server as the DB) */
51
	protected $lockMgr;
52
53
	/**
54
	 * Additional params include:
55
	 *   - dbDirectory : directory containing the DB and the lock file directory
56
	 *                   [defaults to $wgSQLiteDataDir]
57
	 *   - dbFilePath  : use this to force the path of the DB file
58
	 *   - trxMode     : one of (deferred, immediate, exclusive)
59
	 * @param array $p
60
	 */
61
	function __construct( array $p ) {
62
		if ( isset( $p['dbFilePath'] ) ) {
63
			parent::__construct( $p );
64
			// Standalone .sqlite file mode.
65
			// Super doesn't open when $user is false, but we can work with $dbName,
66
			// which is derived from the file path in this case.
67
			$this->openFile( $p['dbFilePath'] );
68
			$lockDomain = md5( $p['dbFilePath'] );
69
		} elseif ( !isset( $p['dbDirectory'] ) ) {
70
			throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
71
		} else {
72
			$this->dbDir = $p['dbDirectory'];
73
			$this->mDBname = $p['dbname'];
74
			$lockDomain = $this->mDBname;
75
			// Stock wiki mode using standard file names per DB.
76
			parent::__construct( $p );
77
			// Super doesn't open when $user is false, but we can work with $dbName
78
			if ( $p['dbname'] && !$this->isOpen() ) {
79
				if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) {
80
					$done = [];
81
					foreach ( $this->tableAliases as $params ) {
82
						if ( isset( $done[$params['dbname']] ) ) {
83
							continue;
84
						}
85
						$this->attachDatabase( $params['dbname'] );
86
						$done[$params['dbname']] = 1;
87
					}
88
				}
89
			}
90
		}
91
92
		$this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
93
		if ( $this->trxMode &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->trxMode of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
94
			!in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
95
		) {
96
			$this->trxMode = null;
97
			$this->queryLogger->warning( "Invalid SQLite transaction mode provided." );
98
		}
99
100
		$this->lockMgr = new FSLockManager( [
101
			'domain' => $lockDomain,
102
			'lockDirectory' => "{$this->dbDir}/locks"
103
		] );
104
	}
105
106
	/**
107
	 * @param string $filename
108
	 * @param array $p Options map; supports:
109
	 *   - flags       : (same as __construct counterpart)
110
	 *   - trxMode     : (same as __construct counterpart)
111
	 *   - dbDirectory : (same as __construct counterpart)
112
	 * @return DatabaseSqlite
113
	 * @since 1.25
114
	 */
115
	public static function newStandaloneInstance( $filename, array $p = [] ) {
116
		$p['dbFilePath'] = $filename;
117
		$p['schema'] = false;
118
		$p['tablePrefix'] = '';
119
120
		return DatabaseBase::factory( 'sqlite', $p );
121
	}
122
123
	/**
124
	 * @return string
125
	 */
126
	function getType() {
127
		return 'sqlite';
128
	}
129
130
	/**
131
	 * @todo Check if it should be true like parent class
132
	 *
133
	 * @return bool
134
	 */
135
	function implicitGroupby() {
136
		return false;
137
	}
138
139
	/** Open an SQLite database and return a resource handle to it
140
	 *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
141
	 *
142
	 * @param string $server
143
	 * @param string $user
144
	 * @param string $pass
145
	 * @param string $dbName
146
	 *
147
	 * @throws DBConnectionError
148
	 * @return PDO
149
	 */
150
	function open( $server, $user, $pass, $dbName ) {
151
		$this->close();
152
		$fileName = self::generateFileName( $this->dbDir, $dbName );
153
		if ( !is_readable( $fileName ) ) {
154
			$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<PDO> of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
155
			throw new DBConnectionError( $this, "SQLite database not accessible" );
156
		}
157
		$this->openFile( $fileName );
158
159
		return $this->mConn;
160
	}
161
162
	/**
163
	 * Opens a database file
164
	 *
165
	 * @param string $fileName
166
	 * @throws DBConnectionError
167
	 * @return PDO|bool SQL connection or false if failed
168
	 */
169
	protected function openFile( $fileName ) {
170
		$err = false;
171
172
		$this->dbPath = $fileName;
173
		try {
174
			if ( $this->mFlags & DBO_PERSISTENT ) {
175
				$this->mConn = new PDO( "sqlite:$fileName", '', '',
176
					[ PDO::ATTR_PERSISTENT => true ] );
177
			} else {
178
				$this->mConn = new PDO( "sqlite:$fileName", '', '' );
179
			}
180
		} catch ( PDOException $e ) {
181
			$err = $e->getMessage();
182
		}
183
184
		if ( !$this->mConn ) {
185
			$this->queryLogger->debug( "DB connection error: $err\n" );
186
			throw new DBConnectionError( $this, $err );
0 ignored issues
show
Security Bug introduced by
It seems like $err defined by false on line 170 can also be of type false; however, DBConnectionError::__construct() 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...
187
		}
188
189
		$this->mOpened = !!$this->mConn;
190
		if ( $this->mOpened ) {
191
			# Set error codes only, don't raise exceptions
192
			$this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
193
			# Enforce LIKE to be case sensitive, just like MySQL
194
			$this->query( 'PRAGMA case_sensitive_like = 1' );
195
196
			return $this->mConn;
197
		}
198
199
		return false;
200
	}
201
202
	/**
203
	 * @return string SQLite DB file path
204
	 * @since 1.25
205
	 */
206
	public function getDbFilePath() {
207
		return $this->dbPath;
208
	}
209
210
	/**
211
	 * Does not actually close the connection, just destroys the reference for GC to do its work
212
	 * @return bool
213
	 */
214
	protected function closeConnection() {
215
		$this->mConn = null;
216
217
		return true;
218
	}
219
220
	/**
221
	 * Generates a database file name. Explicitly public for installer.
222
	 * @param string $dir Directory where database resides
223
	 * @param string $dbName Database name
224
	 * @return string
225
	 */
226
	public static function generateFileName( $dir, $dbName ) {
227
		return "$dir/$dbName.sqlite";
228
	}
229
230
	/**
231
	 * Check if the searchindext table is FTS enabled.
232
	 * @return bool False if not enabled.
233
	 */
234
	function checkForEnabledSearch() {
235
		if ( self::$fulltextEnabled === null ) {
236
			self::$fulltextEnabled = false;
237
			$table = $this->tableName( 'searchindex' );
238
			$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
239
			if ( $res ) {
240
				$row = $res->fetchRow();
241
				self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
242
			}
243
		}
244
245
		return self::$fulltextEnabled;
246
	}
247
248
	/**
249
	 * Returns version of currently supported SQLite fulltext search module or false if none present.
250
	 * @return string
251
	 */
252
	static function getFulltextSearchModule() {
253
		static $cachedResult = null;
254
		if ( $cachedResult !== null ) {
255
			return $cachedResult;
256
		}
257
		$cachedResult = false;
258
		$table = 'dummy_search_test';
259
260
		$db = self::newStandaloneInstance( ':memory:' );
261
		if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
262
			$cachedResult = 'FTS3';
263
		}
264
		$db->close();
265
266
		return $cachedResult;
267
	}
268
269
	/**
270
	 * Attaches external database to our connection, see http://sqlite.org/lang_attach.html
271
	 * for details.
272
	 *
273
	 * @param string $name Database name to be used in queries like
274
	 *   SELECT foo FROM dbname.table
275
	 * @param bool|string $file Database file name. If omitted, will be generated
276
	 *   using $name and configured data directory
277
	 * @param string $fname Calling function name
278
	 * @return ResultWrapper
279
	 */
280
	function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
281
		if ( !$file ) {
282
			$file = self::generateFileName( $this->dbDir, $name );
283
		}
284
		$file = $this->addQuotes( $file );
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->addQuotes($file) on line 284 can also be of type boolean; however, DatabaseSqlite::addQuotes() does only seem to accept object<Blob>|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...
285
286
		return $this->query( "ATTACH DATABASE $file AS $name", $fname );
287
	}
288
289
	function isWriteQuery( $sql ) {
290
		return parent::isWriteQuery( $sql ) && !preg_match( '/^(ATTACH|PRAGMA)\b/i', $sql );
291
	}
292
293
	/**
294
	 * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
295
	 *
296
	 * @param string $sql
297
	 * @return bool|ResultWrapper
298
	 */
299
	protected function doQuery( $sql ) {
300
		$res = $this->mConn->query( $sql );
301
		if ( $res === false ) {
302
			return false;
303
		}
304
305
		$r = $res instanceof ResultWrapper ? $res->result : $res;
306
		$this->mAffectedRows = $r->rowCount();
307
		$res = new ResultWrapper( $this, $r->fetchAll() );
308
309
		return $res;
310
	}
311
312
	/**
313
	 * @param ResultWrapper|mixed $res
314
	 */
315
	function freeResult( $res ) {
316
		if ( $res instanceof ResultWrapper ) {
317
			$res->result = null;
318
		} else {
319
			$res = null;
0 ignored issues
show
Unused Code introduced by
$res 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...
320
		}
321
	}
322
323
	/**
324
	 * @param ResultWrapper|array $res
325
	 * @return stdClass|bool
326
	 */
327
	function fetchObject( $res ) {
328
		if ( $res instanceof ResultWrapper ) {
329
			$r =& $res->result;
330
		} else {
331
			$r =& $res;
332
		}
333
334
		$cur = current( $r );
335
		if ( is_array( $cur ) ) {
336
			next( $r );
337
			$obj = new stdClass;
338
			foreach ( $cur as $k => $v ) {
339
				if ( !is_numeric( $k ) ) {
340
					$obj->$k = $v;
341
				}
342
			}
343
344
			return $obj;
345
		}
346
347
		return false;
348
	}
349
350
	/**
351
	 * @param ResultWrapper|mixed $res
352
	 * @return array|bool
353
	 */
354
	function fetchRow( $res ) {
355
		if ( $res instanceof ResultWrapper ) {
356
			$r =& $res->result;
357
		} else {
358
			$r =& $res;
359
		}
360
		$cur = current( $r );
361
		if ( is_array( $cur ) ) {
362
			next( $r );
363
364
			return $cur;
365
		}
366
367
		return false;
368
	}
369
370
	/**
371
	 * The PDO::Statement class implements the array interface so count() will work
372
	 *
373
	 * @param ResultWrapper|array $res
374
	 * @return int
375
	 */
376
	function numRows( $res ) {
377
		$r = $res instanceof ResultWrapper ? $res->result : $res;
378
379
		return count( $r );
380
	}
381
382
	/**
383
	 * @param ResultWrapper $res
384
	 * @return int
385
	 */
386
	function numFields( $res ) {
387
		$r = $res instanceof ResultWrapper ? $res->result : $res;
388
		if ( is_array( $r ) && count( $r ) > 0 ) {
389
			// The size of the result array is twice the number of fields. (Bug: 65578)
390
			return count( $r[0] ) / 2;
391
		} else {
392
			// If the result is empty return 0
393
			return 0;
394
		}
395
	}
396
397
	/**
398
	 * @param ResultWrapper $res
399
	 * @param int $n
400
	 * @return bool
401
	 */
402
	function fieldName( $res, $n ) {
403
		$r = $res instanceof ResultWrapper ? $res->result : $res;
404
		if ( is_array( $r ) ) {
405
			$keys = array_keys( $r[0] );
406
407
			return $keys[$n];
408
		}
409
410
		return false;
411
	}
412
413
	/**
414
	 * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
415
	 *
416
	 * @param string $name
417
	 * @param string $format
418
	 * @return string
419
	 */
420
	function tableName( $name, $format = 'quoted' ) {
421
		// table names starting with sqlite_ are reserved
422
		if ( strpos( $name, 'sqlite_' ) === 0 ) {
423
			return $name;
424
		}
425
426
		return str_replace( '"', '', parent::tableName( $name, $format ) );
427
	}
428
429
	/**
430
	 * Index names have DB scope
431
	 *
432
	 * @param string $index
433
	 * @return string
434
	 */
435
	protected function indexName( $index ) {
436
		return $index;
437
	}
438
439
	/**
440
	 * This must be called after nextSequenceVal
441
	 *
442
	 * @return int
443
	 */
444
	function insertId() {
445
		// PDO::lastInsertId yields a string :(
446
		return intval( $this->mConn->lastInsertId() );
447
	}
448
449
	/**
450
	 * @param ResultWrapper|array $res
451
	 * @param int $row
452
	 */
453
	function dataSeek( $res, $row ) {
454
		if ( $res instanceof ResultWrapper ) {
455
			$r =& $res->result;
456
		} else {
457
			$r =& $res;
458
		}
459
		reset( $r );
460
		if ( $row > 0 ) {
461
			for ( $i = 0; $i < $row; $i++ ) {
462
				next( $r );
463
			}
464
		}
465
	}
466
467
	/**
468
	 * @return string
469
	 */
470
	function lastError() {
471
		if ( !is_object( $this->mConn ) ) {
472
			return "Cannot return last error, no db connection";
473
		}
474
		$e = $this->mConn->errorInfo();
475
476
		return isset( $e[2] ) ? $e[2] : '';
477
	}
478
479
	/**
480
	 * @return string
481
	 */
482
	function lastErrno() {
483
		if ( !is_object( $this->mConn ) ) {
484
			return "Cannot return last error, no db connection";
485
		} else {
486
			$info = $this->mConn->errorInfo();
487
488
			return $info[1];
489
		}
490
	}
491
492
	/**
493
	 * @return int
494
	 */
495
	function affectedRows() {
496
		return $this->mAffectedRows;
497
	}
498
499
	/**
500
	 * Returns information about an index
501
	 * Returns false if the index does not exist
502
	 * - if errors are explicitly ignored, returns NULL on failure
503
	 *
504
	 * @param string $table
505
	 * @param string $index
506
	 * @param string $fname
507
	 * @return array
508
	 */
509
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
510
		$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
511
		$res = $this->query( $sql, $fname );
512
		if ( !$res ) {
513
			return null;
514
		}
515
		if ( $res->numRows() == 0 ) {
516
			return false;
517
		}
518
		$info = [];
519
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
520
			$info[] = $row->name;
521
		}
522
523
		return $info;
524
	}
525
526
	/**
527
	 * @param string $table
528
	 * @param string $index
529
	 * @param string $fname
530
	 * @return bool|null
531
	 */
532
	function indexUnique( $table, $index, $fname = __METHOD__ ) {
533
		$row = $this->selectRow( 'sqlite_master', '*',
534
			[
535
				'type' => 'index',
536
				'name' => $this->indexName( $index ),
537
			], $fname );
538
		if ( !$row || !isset( $row->sql ) ) {
539
			return null;
540
		}
541
542
		// $row->sql will be of the form CREATE [UNIQUE] INDEX ...
543
		$indexPos = strpos( $row->sql, 'INDEX' );
544
		if ( $indexPos === false ) {
545
			return null;
546
		}
547
		$firstPart = substr( $row->sql, 0, $indexPos );
548
		$options = explode( ' ', $firstPart );
549
550
		return in_array( 'UNIQUE', $options );
551
	}
552
553
	/**
554
	 * Filter the options used in SELECT statements
555
	 *
556
	 * @param array $options
557
	 * @return array
558
	 */
559
	function makeSelectOptions( $options ) {
560
		foreach ( $options as $k => $v ) {
561
			if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
562
				$options[$k] = '';
563
			}
564
		}
565
566
		return parent::makeSelectOptions( $options );
567
	}
568
569
	/**
570
	 * @param array $options
571
	 * @return string
572
	 */
573
	protected function makeUpdateOptionsArray( $options ) {
574
		$options = parent::makeUpdateOptionsArray( $options );
575
		$options = self::fixIgnore( $options );
576
577
		return $options;
578
	}
579
580
	/**
581
	 * @param array $options
582
	 * @return array
583
	 */
584
	static function fixIgnore( $options ) {
585
		# SQLite uses OR IGNORE not just IGNORE
586
		foreach ( $options as $k => $v ) {
587
			if ( $v == 'IGNORE' ) {
588
				$options[$k] = 'OR IGNORE';
589
			}
590
		}
591
592
		return $options;
593
	}
594
595
	/**
596
	 * @param array $options
597
	 * @return string
598
	 */
599
	function makeInsertOptions( $options ) {
600
		$options = self::fixIgnore( $options );
601
602
		return parent::makeInsertOptions( $options );
603
	}
604
605
	/**
606
	 * Based on generic method (parent) with some prior SQLite-sepcific adjustments
607
	 * @param string $table
608
	 * @param array $a
609
	 * @param string $fname
610
	 * @param array $options
611
	 * @return bool
612
	 */
613
	function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
614
		if ( !count( $a ) ) {
615
			return true;
616
		}
617
618
		# SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
619
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
620
			$ret = true;
621
			foreach ( $a as $v ) {
622
				if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) {
623
					$ret = false;
624
				}
625
			}
626
		} else {
627
			$ret = parent::insert( $table, $a, "$fname/single-row", $options );
628
		}
629
630
		return $ret;
631
	}
632
633
	/**
634
	 * @param string $table
635
	 * @param array $uniqueIndexes Unused
636
	 * @param string|array $rows
637
	 * @param string $fname
638
	 * @return bool|ResultWrapper
639
	 */
640
	function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
641
		if ( !count( $rows ) ) {
642
			return true;
643
		}
644
645
		# SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
646
		if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
647
			$ret = true;
648
			foreach ( $rows as $v ) {
0 ignored issues
show
Bug introduced by
The expression $rows of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
649
				if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) {
650
					$ret = false;
651
				}
652
			}
653
		} else {
654
			$ret = $this->nativeReplace( $table, $rows, "$fname/single-row" );
655
		}
656
657
		return $ret;
658
	}
659
660
	/**
661
	 * Returns the size of a text field, or -1 for "unlimited"
662
	 * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
663
	 *
664
	 * @param string $table
665
	 * @param string $field
666
	 * @return int
667
	 */
668
	function textFieldSize( $table, $field ) {
669
		return -1;
670
	}
671
672
	/**
673
	 * @return bool
674
	 */
675
	function unionSupportsOrderAndLimit() {
676
		return false;
677
	}
678
679
	/**
680
	 * @param string $sqls
681
	 * @param bool $all Whether to "UNION ALL" or not
682
	 * @return string
683
	 */
684
	function unionQueries( $sqls, $all ) {
685
		$glue = $all ? ' UNION ALL ' : ' UNION ';
686
687
		return implode( $glue, $sqls );
688
	}
689
690
	/**
691
	 * @return bool
692
	 */
693
	function wasDeadlock() {
694
		return $this->lastErrno() == 5; // SQLITE_BUSY
695
	}
696
697
	/**
698
	 * @return bool
699
	 */
700
	function wasErrorReissuable() {
701
		return $this->lastErrno() == 17; // SQLITE_SCHEMA;
702
	}
703
704
	/**
705
	 * @return bool
706
	 */
707
	function wasReadOnlyError() {
708
		return $this->lastErrno() == 8; // SQLITE_READONLY;
709
	}
710
711
	/**
712
	 * @return string Wikitext of a link to the server software's web site
713
	 */
714
	public function getSoftwareLink() {
715
		return "[{{int:version-db-sqlite-url}} SQLite]";
716
	}
717
718
	/**
719
	 * @return string Version information from the database
720
	 */
721
	function getServerVersion() {
722
		$ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
723
724
		return $ver;
725
	}
726
727
	/**
728
	 * Get information about a given field
729
	 * Returns false if the field does not exist.
730
	 *
731
	 * @param string $table
732
	 * @param string $field
733
	 * @return SQLiteField|bool False on failure
734
	 */
735
	function fieldInfo( $table, $field ) {
736
		$tableName = $this->tableName( $table );
737
		$sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
738
		$res = $this->query( $sql, __METHOD__ );
739
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
740
			if ( $row->name == $field ) {
741
				return new SQLiteField( $row, $tableName );
742
			}
743
		}
744
745
		return false;
746
	}
747
748
	protected function doBegin( $fname = '' ) {
749
		if ( $this->trxMode ) {
750
			$this->query( "BEGIN {$this->trxMode}", $fname );
751
		} else {
752
			$this->query( 'BEGIN', $fname );
753
		}
754
		$this->mTrxLevel = 1;
755
	}
756
757
	/**
758
	 * @param string $s
759
	 * @return string
760
	 */
761
	function strencode( $s ) {
762
		return substr( $this->addQuotes( $s ), 1, -1 );
763
	}
764
765
	/**
766
	 * @param string $b
767
	 * @return Blob
768
	 */
769
	function encodeBlob( $b ) {
770
		return new Blob( $b );
771
	}
772
773
	/**
774
	 * @param Blob|string $b
775
	 * @return string
776
	 */
777
	function decodeBlob( $b ) {
778
		if ( $b instanceof Blob ) {
779
			$b = $b->fetch();
780
		}
781
782
		return $b;
783
	}
784
785
	/**
786
	 * @param Blob|string $s
787
	 * @return string
788
	 */
789
	function addQuotes( $s ) {
790
		if ( $s instanceof Blob ) {
791
			return "x'" . bin2hex( $s->fetch() ) . "'";
792
		} elseif ( is_bool( $s ) ) {
793
			return (int)$s;
794
		} elseif ( strpos( $s, "\0" ) !== false ) {
795
			// SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
796
			// This is a known limitation of SQLite's mprintf function which PDO
797
			// should work around, but doesn't. I have reported this to php.net as bug #63419:
798
			// https://bugs.php.net/bug.php?id=63419
799
			// There was already a similar report for SQLite3::escapeString, bug #62361:
800
			// https://bugs.php.net/bug.php?id=62361
801
			// There is an additional bug regarding sorting this data after insert
802
			// on older versions of sqlite shipped with ubuntu 12.04
803
			// https://phabricator.wikimedia.org/T74367
804
			$this->queryLogger->debug(
805
				__FUNCTION__ .
806
				': Quoting value containing null byte. ' .
807
				'For consistency all binary data should have been ' .
808
				'first processed with self::encodeBlob()'
809
			);
810
			return "x'" . bin2hex( $s ) . "'";
811
		} else {
812
			return $this->mConn->quote( $s );
813
		}
814
	}
815
816
	/**
817
	 * @return string
818
	 */
819 View Code Duplication
	function buildLike() {
820
		$params = func_get_args();
821
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
822
			$params = $params[0];
823
		}
824
825
		return parent::buildLike( $params ) . "ESCAPE '\' ";
826
	}
827
828
	/**
829
	 * @param string $field Field or column to cast
830
	 * @return string
831
	 * @since 1.28
832
	 */
833
	public function buildStringCast( $field ) {
834
		return 'CAST ( ' . $field . ' AS TEXT )';
835
	}
836
837
	/**
838
	 * No-op version of deadlockLoop
839
	 *
840
	 * @return mixed
841
	 */
842
	public function deadlockLoop( /*...*/ ) {
843
		$args = func_get_args();
844
		$function = array_shift( $args );
845
846
		return call_user_func_array( $function, $args );
847
	}
848
849
	/**
850
	 * @param string $s
851
	 * @return string
852
	 */
853
	protected function replaceVars( $s ) {
854
		$s = parent::replaceVars( $s );
855
		if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
856
			// CREATE TABLE hacks to allow schema file sharing with MySQL
857
858
			// binary/varbinary column type -> blob
859
			$s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
860
			// no such thing as unsigned
861
			$s = preg_replace( '/\b(un)?signed\b/i', '', $s );
862
			// INT -> INTEGER
863
			$s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s );
864
			// floating point types -> REAL
865
			$s = preg_replace(
866
				'/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i',
867
				'REAL',
868
				$s
869
			);
870
			// varchar -> TEXT
871
			$s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s );
872
			// TEXT normalization
873
			$s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
874
			// BLOB normalization
875
			$s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
876
			// BOOL -> INTEGER
877
			$s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
878
			// DATETIME -> TEXT
879
			$s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
880
			// No ENUM type
881
			$s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s );
882
			// binary collation type -> nothing
883
			$s = preg_replace( '/\bbinary\b/i', '', $s );
884
			// auto_increment -> autoincrement
885
			$s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
886
			// No explicit options
887
			$s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
888
			// AUTOINCREMENT should immedidately follow PRIMARY KEY
889
			$s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s );
890
		} elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
891
			// No truncated indexes
892
			$s = preg_replace( '/\(\d+\)/', '', $s );
893
			// No FULLTEXT
894
			$s = preg_replace( '/\bfulltext\b/i', '', $s );
895
		} elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) {
896
			// DROP INDEX is database-wide, not table-specific, so no ON <table> clause.
897
			$s = preg_replace( '/\sON\s+[^\s]*/i', '', $s );
898
		} elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) {
899
			// INSERT IGNORE --> INSERT OR IGNORE
900
			$s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s );
901
		}
902
903
		return $s;
904
	}
905
906
	public function lock( $lockName, $method, $timeout = 5 ) {
907
		if ( !is_dir( "{$this->dbDir}/locks" ) ) { // create dir as needed
908
			if ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) ) {
909
				throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
910
			}
911
		}
912
913
		return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
914
	}
915
916
	public function unlock( $lockName, $method ) {
917
		return $this->lockMgr->unlock( [ $lockName ], LockManager::LOCK_EX )->isOK();
918
	}
919
920
	/**
921
	 * Build a concatenation list to feed into a SQL query
922
	 *
923
	 * @param string[] $stringList
924
	 * @return string
925
	 */
926
	function buildConcat( $stringList ) {
927
		return '(' . implode( ') || (', $stringList ) . ')';
928
	}
929
930 View Code Duplication
	public function buildGroupConcatField(
931
		$delim, $table, $field, $conds = '', $join_conds = []
932
	) {
933
		$fld = "group_concat($field," . $this->addQuotes( $delim ) . ')';
934
935
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
936
	}
937
938
	/**
939
	 * @param string $oldName
940
	 * @param string $newName
941
	 * @param bool $temporary
942
	 * @param string $fname
943
	 * @return bool|ResultWrapper
944
	 * @throws RuntimeException
945
	 */
946
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
947
		$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" .
948
			$this->addQuotes( $oldName ) . " AND type='table'", $fname );
949
		$obj = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SELECT sql...ype=\'table\'', $fname) on line 947 can also be of type boolean; however, DatabaseSqlite::fetchObject() does only seem to accept object<ResultWrapper>|array, 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...
950
		if ( !$obj ) {
951
			throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
952
		}
953
		$sql = $obj->sql;
954
		$sql = preg_replace(
955
			'/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/',
956
			$this->addIdentifierQuotes( $newName ),
957
			$sql,
958
			1
959
		);
960
		if ( $temporary ) {
961
			if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) {
962
				$this->queryLogger->debug(
963
					"Table $oldName is virtual, can't create a temporary duplicate.\n" );
964
			} else {
965
				$sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql );
966
			}
967
		}
968
969
		$res = $this->query( $sql, $fname );
970
971
		// Take over indexes
972
		$indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
973
		foreach ( $indexList as $index ) {
0 ignored issues
show
Bug introduced by
The expression $indexList of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
974
			if ( strpos( $index->name, 'sqlite_autoindex' ) === 0 ) {
975
				continue;
976
			}
977
978
			if ( $index->unique ) {
979
				$sql = 'CREATE UNIQUE INDEX';
980
			} else {
981
				$sql = 'CREATE INDEX';
982
			}
983
			// Try to come up with a new index name, given indexes have database scope in SQLite
984
			$indexName = $newName . '_' . $index->name;
985
			$sql .= ' ' . $indexName . ' ON ' . $newName;
986
987
			$indexInfo = $this->query( 'PRAGMA INDEX_INFO(' . $this->addQuotes( $index->name ) . ')' );
988
			$fields = [];
989
			foreach ( $indexInfo as $indexInfoRow ) {
0 ignored issues
show
Bug introduced by
The expression $indexInfo of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
990
				$fields[$indexInfoRow->seqno] = $indexInfoRow->name;
991
			}
992
993
			$sql .= '(' . implode( ',', $fields ) . ')';
994
995
			$this->query( $sql );
996
		}
997
998
		return $res;
999
	}
1000
1001
	/**
1002
	 * List all tables on the database
1003
	 *
1004
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1005
	 * @param string $fname Calling function name
1006
	 *
1007
	 * @return array
1008
	 */
1009
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1010
		$result = $this->select(
1011
			'sqlite_master',
1012
			'name',
1013
			"type='table'"
1014
		);
1015
1016
		$endArray = [];
1017
1018 View Code Duplication
		foreach ( $result as $table ) {
0 ignored issues
show
Bug introduced by
The expression $result of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1019
			$vars = get_object_vars( $table );
1020
			$table = array_pop( $vars );
1021
1022
			if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prefix of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1023
				if ( strpos( $table, 'sqlite_' ) !== 0 ) {
1024
					$endArray[] = $table;
1025
				}
1026
			}
1027
		}
1028
1029
		return $endArray;
1030
	}
1031
1032
	/**
1033
	 * Override due to no CASCADE support
1034
	 *
1035
	 * @param string $tableName
1036
	 * @param string $fName
1037
	 * @return bool|ResultWrapper
1038
	 * @throws DBReadOnlyError
1039
	 */
1040 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1041
		if ( !$this->tableExists( $tableName, $fName ) ) {
1042
			return false;
1043
		}
1044
		$sql = "DROP TABLE " . $this->tableName( $tableName );
1045
1046
		return $this->query( $sql, $fName );
1047
	}
1048
1049
	protected function requiresDatabaseUser() {
1050
		return false; // just a file
1051
	}
1052
1053
	/**
1054
	 * @return string
1055
	 */
1056
	public function __toString() {
1057
		return 'SQLite ' . (string)$this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
1058
	}
1059
}
1060