Completed
Branch master (7e350b)
by
unknown
30:36
created

DatabaseOracle::buildStringCast()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * This is the Oracle database abstraction layer.
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 Database
22
 */
23
24
/**
25
 * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
26
 * other things. We use a wrapper class to handle that and other
27
 * Oracle-specific bits, like converting column names back to lowercase.
28
 * @ingroup Database
29
 */
30
class ORAResult {
31
	private $rows;
32
	private $cursor;
33
	private $nrows;
34
35
	private $columns = [];
36
37
	private function array_unique_md( $array_in ) {
38
		$array_out = [];
39
		$array_hashes = [];
40
41
		foreach ( $array_in as $item ) {
42
			$hash = md5( serialize( $item ) );
43
			if ( !isset( $array_hashes[$hash] ) ) {
44
				$array_hashes[$hash] = $hash;
45
				$array_out[] = $item;
46
			}
47
		}
48
49
		return $array_out;
50
	}
51
52
	/**
53
	 * @param DatabaseBase $db
54
	 * @param resource $stmt A valid OCI statement identifier
55
	 * @param bool $unique
56
	 */
57
	function __construct( &$db, $stmt, $unique = false ) {
58
		$this->db =& $db;
0 ignored issues
show
Bug introduced by
The property db 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...
59
60
		$this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM );
61
		if ( $this->nrows === false ) {
62
			$e = oci_error( $stmt );
63
			$db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ );
64
			$this->free();
65
66
			return;
67
		}
68
69
		if ( $unique ) {
70
			$this->rows = $this->array_unique_md( $this->rows );
71
			$this->nrows = count( $this->rows );
72
		}
73
74
		if ( $this->nrows > 0 ) {
75
			foreach ( $this->rows[0] as $k => $v ) {
76
				$this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) );
77
			}
78
		}
79
80
		$this->cursor = 0;
81
		oci_free_statement( $stmt );
82
	}
83
84
	public function free() {
85
		unset( $this->db );
86
	}
87
88
	public function seek( $row ) {
89
		$this->cursor = min( $row, $this->nrows );
90
	}
91
92
	public function numRows() {
93
		return $this->nrows;
94
	}
95
96
	public function numFields() {
97
		return count( $this->columns );
98
	}
99
100
	public function fetchObject() {
101
		if ( $this->cursor >= $this->nrows ) {
102
			return false;
103
		}
104
		$row = $this->rows[$this->cursor++];
105
		$ret = new stdClass();
106
		foreach ( $row as $k => $v ) {
107
			$lc = $this->columns[$k];
108
			$ret->$lc = $v;
109
		}
110
111
		return $ret;
112
	}
113
114
	public function fetchRow() {
115
		if ( $this->cursor >= $this->nrows ) {
116
			return false;
117
		}
118
119
		$row = $this->rows[$this->cursor++];
120
		$ret = [];
121
		foreach ( $row as $k => $v ) {
122
			$lc = $this->columns[$k];
123
			$ret[$lc] = $v;
124
			$ret[$k] = $v;
125
		}
126
127
		return $ret;
128
	}
129
}
130
131
/**
132
 * Utility class.
133
 * @ingroup Database
134
 */
135
class ORAField implements Field {
136
	private $name, $tablename, $default, $max_length, $nullable,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
137
		$is_pk, $is_unique, $is_multiple, $is_key, $type;
138
139
	function __construct( $info ) {
140
		$this->name = $info['column_name'];
141
		$this->tablename = $info['table_name'];
142
		$this->default = $info['data_default'];
143
		$this->max_length = $info['data_length'];
144
		$this->nullable = $info['not_null'];
145
		$this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0;
146
		$this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0;
147
		$this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0;
148
		$this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
149
		$this->type = $info['data_type'];
150
	}
151
152
	function name() {
153
		return $this->name;
154
	}
155
156
	function tableName() {
157
		return $this->tablename;
158
	}
159
160
	function defaultValue() {
161
		return $this->default;
162
	}
163
164
	function maxLength() {
165
		return $this->max_length;
166
	}
167
168
	function isNullable() {
169
		return $this->nullable;
170
	}
171
172
	function isKey() {
173
		return $this->is_key;
174
	}
175
176
	function isMultipleKey() {
177
		return $this->is_multiple;
178
	}
179
180
	function type() {
181
		return $this->type;
182
	}
183
}
184
185
/**
186
 * @ingroup Database
187
 */
188
class DatabaseOracle extends Database {
189
	/** @var resource */
190
	protected $mLastResult = null;
191
192
	/** @var int The number of rows affected as an integer */
193
	protected $mAffectedRows;
194
195
	/** @var int */
196
	private $mInsertId = null;
197
198
	/** @var bool */
199
	private $ignoreDupValOnIndex = false;
200
201
	/** @var bool|array */
202
	private $sequenceData = null;
203
204
	/** @var string Character set for Oracle database */
205
	private $defaultCharset = 'AL32UTF8';
206
207
	/** @var array */
208
	private $mFieldInfoCache = [];
209
210
	function __construct( array $p ) {
211
		global $wgDBprefix;
212
213
		if ( $p['tablePrefix'] == 'get from global' ) {
214
			$p['tablePrefix'] = $wgDBprefix;
215
		}
216
		$p['tablePrefix'] = strtoupper( $p['tablePrefix'] );
217
		parent::__construct( $p );
218
		Hooks::run( 'DatabaseOraclePostInit', [ $this ] );
219
	}
220
221
	function __destruct() {
222
		if ( $this->mOpened ) {
223
			MediaWiki\suppressWarnings();
224
			$this->close();
225
			MediaWiki\restoreWarnings();
226
		}
227
	}
228
229
	function getType() {
230
		return 'oracle';
231
	}
232
233
	function cascadingDeletes() {
234
		return true;
235
	}
236
237
	function cleanupTriggers() {
238
		return true;
239
	}
240
241
	function strictIPs() {
242
		return true;
243
	}
244
245
	function realTimestamps() {
246
		return true;
247
	}
248
249
	function implicitGroupby() {
250
		return false;
251
	}
252
253
	function implicitOrderby() {
254
		return false;
255
	}
256
257
	function searchableIPs() {
258
		return true;
259
	}
260
261
	/**
262
	 * Usually aborts on failure
263
	 * @param string $server
264
	 * @param string $user
265
	 * @param string $password
266
	 * @param string $dbName
267
	 * @throws DBConnectionError
268
	 * @return DatabaseBase|null
269
	 */
270
	function open( $server, $user, $password, $dbName ) {
271
		global $wgDBOracleDRCP;
272
		if ( !function_exists( 'oci_connect' ) ) {
273
			throw new DBConnectionError(
274
				$this,
275
				"Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n " .
276
					"(Note: if you recently installed PHP, you may need to restart your webserver\n " .
277
					"and database)\n" );
278
		}
279
280
		$this->close();
281
		$this->mUser = $user;
282
		$this->mPassword = $password;
283
		// changed internal variables functions
284
		// mServer now holds the TNS endpoint
285
		// mDBname is schema name if different from username
286
		if ( !$server ) {
287
			// backward compatibillity (server used to be null and TNS was supplied in dbname)
288
			$this->mServer = $dbName;
289
			$this->mDBname = $user;
290
		} else {
291
			$this->mServer = $server;
292
			if ( !$dbName ) {
293
				$this->mDBname = $user;
294
			} else {
295
				$this->mDBname = $dbName;
296
			}
297
		}
298
299
		if ( !strlen( $user ) ) { # e.g. the class is being loaded
300
			return null;
301
		}
302
303
		if ( $wgDBOracleDRCP ) {
304
			$this->setFlag( DBO_PERSISTENT );
305
		}
306
307
		$session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
308
309
		MediaWiki\suppressWarnings();
310
		if ( $this->mFlags & DBO_PERSISTENT ) {
311
			$this->mConn = oci_pconnect(
312
				$this->mUser,
313
				$this->mPassword,
314
				$this->mServer,
315
				$this->defaultCharset,
316
				$session_mode
317
			);
318
		} elseif ( $this->mFlags & DBO_DEFAULT ) {
319
			$this->mConn = oci_new_connect(
320
				$this->mUser,
321
				$this->mPassword,
322
				$this->mServer,
323
				$this->defaultCharset,
324
				$session_mode
325
			);
326
		} else {
327
			$this->mConn = oci_connect(
328
				$this->mUser,
329
				$this->mPassword,
330
				$this->mServer,
331
				$this->defaultCharset,
332
				$session_mode
333
			);
334
		}
335
		MediaWiki\restoreWarnings();
336
337
		if ( $this->mUser != $this->mDBname ) {
338
			// change current schema in session
339
			$this->selectDB( $this->mDBname );
340
		}
341
342
		if ( !$this->mConn ) {
343
			throw new DBConnectionError( $this, $this->lastError() );
344
		}
345
346
		$this->mOpened = true;
347
348
		# removed putenv calls because they interfere with the system globaly
349
		$this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
350
		$this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
351
		$this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' );
352
353
		return $this->mConn;
354
	}
355
356
	/**
357
	 * Closes a database connection, if it is open
358
	 * Returns success, true if already closed
359
	 * @return bool
360
	 */
361
	protected function closeConnection() {
362
		return oci_close( $this->mConn );
363
	}
364
365
	function execFlags() {
366
		return $this->mTrxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
367
	}
368
369
	protected function doQuery( $sql ) {
370
		wfDebug( "SQL: [$sql]\n" );
371
		if ( !StringUtils::isUtf8( $sql ) ) {
372
			throw new MWException( "SQL encoding is invalid\n$sql" );
373
		}
374
375
		// handle some oracle specifics
376
		// remove AS column/table/subquery namings
377
		if ( !$this->getFlag( DBO_DDLMODE ) ) {
378
			$sql = preg_replace( '/ as /i', ' ', $sql );
379
		}
380
381
		// Oracle has issues with UNION clause if the statement includes LOB fields
382
		// So we do a UNION ALL and then filter the results array with array_unique
383
		$union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 );
384
		// EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing
385
		// you have to select data from plan table after explain
386
		$explain_id = MWTimestamp::getLocalInstance()->format( 'dmYHis' );
387
388
		$sql = preg_replace(
389
			'/^EXPLAIN /',
390
			'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR',
391
			$sql,
392
			1,
393
			$explain_count
394
		);
395
396
		MediaWiki\suppressWarnings();
397
398
		$this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
399 View Code Duplication
		if ( $stmt === false ) {
400
			$e = oci_error( $this->mConn );
401
			$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
402
403
			return false;
404
		}
405
406
		if ( !oci_execute( $stmt, $this->execFlags() ) ) {
407
			$e = oci_error( $stmt );
408
			if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) {
409
				$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
410
411
				return false;
412
			}
413
		}
414
415
		MediaWiki\restoreWarnings();
416
417
		if ( $explain_count > 0 ) {
418
			return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table ' .
419
				'WHERE statement_id = \'' . $explain_id . '\'' );
420
		} elseif ( oci_statement_type( $stmt ) == 'SELECT' ) {
421
			return new ORAResult( $this, $stmt, $union_unique );
422
		} else {
423
			$this->mAffectedRows = oci_num_rows( $stmt );
424
425
			return true;
426
		}
427
	}
428
429
	function queryIgnore( $sql, $fname = '' ) {
430
		return $this->query( $sql, $fname, true );
431
	}
432
433
	/**
434
	 * Frees resources associated with the LOB descriptor
435
	 * @param ResultWrapper|ORAResult $res
436
	 */
437
	function freeResult( $res ) {
438
		if ( $res instanceof ResultWrapper ) {
439
			$res = $res->result;
440
		}
441
442
		$res->free();
0 ignored issues
show
Bug introduced by
It seems like $res is not always an object, but can also be of type resource. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
443
	}
444
445
	/**
446
	 * @param ResultWrapper|ORAResult $res
447
	 * @return mixed
448
	 */
449
	function fetchObject( $res ) {
450
		if ( $res instanceof ResultWrapper ) {
451
			$res = $res->result;
452
		}
453
454
		return $res->fetchObject();
0 ignored issues
show
Bug introduced by
It seems like $res is not always an object, but can also be of type resource. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
455
	}
456
457
	/**
458
	 * @param ResultWrapper|ORAResult $res
459
	 * @return mixed
460
	 */
461
	function fetchRow( $res ) {
462
		if ( $res instanceof ResultWrapper ) {
463
			$res = $res->result;
464
		}
465
466
		return $res->fetchRow();
0 ignored issues
show
Bug introduced by
It seems like $res is not always an object, but can also be of type resource. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
467
	}
468
469
	/**
470
	 * @param ResultWrapper|ORAResult $res
471
	 * @return int
472
	 */
473
	function numRows( $res ) {
474
		if ( $res instanceof ResultWrapper ) {
475
			$res = $res->result;
476
		}
477
478
		return $res->numRows();
0 ignored issues
show
Bug introduced by
It seems like $res is not always an object, but can also be of type resource. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
479
	}
480
481
	/**
482
	 * @param ResultWrapper|ORAResult $res
483
	 * @return int
484
	 */
485
	function numFields( $res ) {
486
		if ( $res instanceof ResultWrapper ) {
487
			$res = $res->result;
488
		}
489
490
		return $res->numFields();
0 ignored issues
show
Bug introduced by
It seems like $res is not always an object, but can also be of type resource. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
491
	}
492
493
	function fieldName( $stmt, $n ) {
494
		return oci_field_name( $stmt, $n );
495
	}
496
497
	/**
498
	 * This must be called after nextSequenceVal
499
	 * @return null|int
500
	 */
501
	function insertId() {
502
		return $this->mInsertId;
503
	}
504
505
	/**
506
	 * @param mixed $res
507
	 * @param int $row
508
	 */
509
	function dataSeek( $res, $row ) {
510
		if ( $res instanceof ORAResult ) {
511
			$res->seek( $row );
512
		} else {
513
			$res->result->seek( $row );
514
		}
515
	}
516
517 View Code Duplication
	function lastError() {
518
		if ( $this->mConn === false ) {
519
			$e = oci_error();
520
		} else {
521
			$e = oci_error( $this->mConn );
522
		}
523
524
		return $e['message'];
525
	}
526
527 View Code Duplication
	function lastErrno() {
528
		if ( $this->mConn === false ) {
529
			$e = oci_error();
530
		} else {
531
			$e = oci_error( $this->mConn );
532
		}
533
534
		return $e['code'];
535
	}
536
537
	function affectedRows() {
538
		return $this->mAffectedRows;
539
	}
540
541
	/**
542
	 * Returns information about an index
543
	 * If errors are explicitly ignored, returns NULL on failure
544
	 * @param string $table
545
	 * @param string $index
546
	 * @param string $fname
547
	 * @return bool
548
	 */
549
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
550
		return false;
551
	}
552
553
	function indexUnique( $table, $index, $fname = __METHOD__ ) {
554
		return false;
555
	}
556
557
	function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
558
		if ( !count( $a ) ) {
559
			return true;
560
		}
561
562
		if ( !is_array( $options ) ) {
563
			$options = [ $options ];
564
		}
565
566
		if ( in_array( 'IGNORE', $options ) ) {
567
			$this->ignoreDupValOnIndex = true;
568
		}
569
570
		if ( !is_array( reset( $a ) ) ) {
571
			$a = [ $a ];
572
		}
573
574
		foreach ( $a as &$row ) {
575
			$this->insertOneRow( $table, $row, $fname );
576
		}
577
		$retVal = true;
578
579
		if ( in_array( 'IGNORE', $options ) ) {
580
			$this->ignoreDupValOnIndex = false;
581
		}
582
583
		return $retVal;
584
	}
585
586
	private function fieldBindStatement( $table, $col, &$val, $includeCol = false ) {
587
		$col_info = $this->fieldInfoMulti( $table, $col );
588
		$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
0 ignored issues
show
Bug introduced by
The method type does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
589
590
		$bind = '';
591
		if ( is_numeric( $col ) ) {
592
			$bind = $val;
593
			$val = null;
594
595
			return $bind;
596
		} elseif ( $includeCol ) {
597
			$bind = "$col = ";
598
		}
599
600
		if ( $val == '' && $val !== 0 && $col_type != 'BLOB' && $col_type != 'CLOB' ) {
601
			$val = null;
602
		}
603
604
		if ( $val === 'NULL' ) {
605
			$val = null;
606
		}
607
608
		if ( $val === null ) {
609
			if ( $col_info != false && $col_info->isNullable() == 0 && $col_info->defaultValue() != null ) {
0 ignored issues
show
Bug introduced by
The method isNullable does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The method defaultValue does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
610
				$bind .= 'DEFAULT';
611
			} else {
612
				$bind .= 'NULL';
613
			}
614
		} else {
615
			$bind .= ':' . $col;
616
		}
617
618
		return $bind;
619
	}
620
621
	/**
622
	 * @param string $table
623
	 * @param array $row
624
	 * @param string $fname
625
	 * @return bool
626
	 * @throws DBUnexpectedError
627
	 */
628
	private function insertOneRow( $table, $row, $fname ) {
629
		global $wgContLang;
630
631
		$table = $this->tableName( $table );
632
		// "INSERT INTO tables (a, b, c)"
633
		$sql = "INSERT INTO " . $table . " (" . implode( ',', array_keys( $row ) ) . ')';
634
		$sql .= " VALUES (";
635
636
		// for each value, append ":key"
637
		$first = true;
638
		foreach ( $row as $col => &$val ) {
639
			if ( !$first ) {
640
				$sql .= ', ';
641
			} else {
642
				$first = false;
643
			}
644
			if ( $this->isQuotedIdentifier( $val ) ) {
645
				$sql .= $this->removeIdentifierQuotes( $val );
646
				unset( $row[$col] );
647
			} else {
648
				$sql .= $this->fieldBindStatement( $table, $col, $val );
649
			}
650
		}
651
		$sql .= ')';
652
653
		$this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
654 View Code Duplication
		if ( $stmt === false ) {
655
			$e = oci_error( $this->mConn );
656
			$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
657
658
			return false;
659
		}
660
		foreach ( $row as $col => &$val ) {
661
			$col_info = $this->fieldInfoMulti( $table, $col );
662
			$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
0 ignored issues
show
Bug introduced by
The method type does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
663
664
			if ( $val === null ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
665
				// do nothing ... null was inserted in statement creation
666
			} elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) {
667
				if ( is_object( $val ) ) {
668
					$val = $val->fetch();
669
				}
670
671
				// backward compatibility
672
				if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
673
					$val = $this->getInfinity();
674
				}
675
676
				$val = ( $wgContLang != null ) ? $wgContLang->checkTitleEncoding( $val ) : $val;
677
				if ( oci_bind_by_name( $stmt, ":$col", $val, -1, SQLT_CHR ) === false ) {
678
					$e = oci_error( $stmt );
679
					$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
680
681
					return false;
682
				}
683 View Code Duplication
			} else {
684
				/** @var OCI_Lob[] $lob */
685
				$lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB );
0 ignored issues
show
Coding Style Comprehensibility introduced by
$lob was never initialized. Although not strictly required by PHP, it is generally a good practice to add $lob = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
686
				if ( $lob[$col] === false ) {
687
					$e = oci_error( $stmt );
688
					throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
689
				}
690
691
				if ( is_object( $val ) ) {
692
					$val = $val->fetch();
693
				}
694
695
				if ( $col_type == 'BLOB' ) {
696
					$lob[$col]->writeTemporary( $val, OCI_TEMP_BLOB );
697
					oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_BLOB );
698
				} else {
699
					$lob[$col]->writeTemporary( $val, OCI_TEMP_CLOB );
700
					oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_CLOB );
701
				}
702
			}
703
		}
704
705
		MediaWiki\suppressWarnings();
706
707 View Code Duplication
		if ( oci_execute( $stmt, $this->execFlags() ) === false ) {
708
			$e = oci_error( $stmt );
709
			if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) {
710
				$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
711
712
				return false;
713
			} else {
714
				$this->mAffectedRows = oci_num_rows( $stmt );
715
			}
716
		} else {
717
			$this->mAffectedRows = oci_num_rows( $stmt );
718
		}
719
720
		MediaWiki\restoreWarnings();
721
722
		if ( isset( $lob ) ) {
723
			foreach ( $lob as $lob_v ) {
724
				$lob_v->free();
725
			}
726
		}
727
728
		if ( !$this->mTrxLevel ) {
729
			oci_commit( $this->mConn );
730
		}
731
732
		return oci_free_statement( $stmt );
733
	}
734
735
	function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
736
		$insertOptions = [], $selectOptions = []
737
	) {
738
		$destTable = $this->tableName( $destTable );
739
		if ( !is_array( $selectOptions ) ) {
740
			$selectOptions = [ $selectOptions ];
741
		}
742
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
743 View Code Duplication
		if ( is_array( $srcTable ) ) {
744
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
745
		} else {
746
			$srcTable = $this->tableName( $srcTable );
747
		}
748
749
		$sequenceData = $this->getSequenceData( $destTable );
750 View Code Duplication
		if ( $sequenceData !== false &&
751
			!isset( $varMap[$sequenceData['column']] )
752
		) {
753
			$varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')';
754
		}
755
756
		// count-alias subselect fields to avoid abigious definition errors
757
		$i = 0;
758
		foreach ( $varMap as &$val ) {
759
			$val = $val . ' field' . ( $i++ );
760
		}
761
762
		$sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
763
			" SELECT $startOpts " . implode( ',', $varMap ) .
764
			" FROM $srcTable $useIndex ";
765
		if ( $conds != '*' ) {
766
			$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
767
		}
768
		$sql .= " $tailOpts";
769
770
		if ( in_array( 'IGNORE', $insertOptions ) ) {
771
			$this->ignoreDupValOnIndex = true;
772
		}
773
774
		$retval = $this->query( $sql, $fname );
775
776
		if ( in_array( 'IGNORE', $insertOptions ) ) {
777
			$this->ignoreDupValOnIndex = false;
778
		}
779
780
		return $retval;
781
	}
782
783
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
784
		$fname = __METHOD__
785
	) {
786
		if ( !count( $rows ) ) {
787
			return true; // nothing to do
788
		}
789
790
		if ( !is_array( reset( $rows ) ) ) {
791
			$rows = [ $rows ];
792
		}
793
794
		$sequenceData = $this->getSequenceData( $table );
795 View Code Duplication
		if ( $sequenceData !== false ) {
796
			// add sequence column to each list of columns, when not set
797
			foreach ( $rows as &$row ) {
798
				if ( !isset( $row[$sequenceData['column']] ) ) {
799
					$row[$sequenceData['column']] =
800
						$this->addIdentifierQuotes( 'GET_SEQUENCE_VALUE(\'' .
801
							$sequenceData['sequence'] . '\')' );
802
				}
803
			}
804
		}
805
806
		return parent::upsert( $table, $rows, $uniqueIndexes, $set, $fname );
807
	}
808
809
	function tableName( $name, $format = 'quoted' ) {
810
		/*
811
		Replace reserved words with better ones
812
		Using uppercase because that's the only way Oracle can handle
813
		quoted tablenames
814
		*/
815
		switch ( $name ) {
816
			case 'user':
817
				$name = 'MWUSER';
818
				break;
819
			case 'text':
820
				$name = 'PAGECONTENT';
821
				break;
822
		}
823
824
		return strtoupper( parent::tableName( $name, $format ) );
825
	}
826
827
	function tableNameInternal( $name ) {
828
		$name = $this->tableName( $name );
829
830
		return preg_replace( '/.*\.(.*)/', '$1', $name );
831
	}
832
833
	/**
834
	 * Return the next in a sequence, save the value for retrieval via insertId()
835
	 *
836
	 * @param string $seqName
837
	 * @return null|int
838
	 */
839
	function nextSequenceValue( $seqName ) {
840
		$res = $this->query( "SELECT $seqName.nextval FROM dual" );
841
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT {$s...me}.nextval FROM dual") on line 840 can also be of type boolean; however, DatabaseOracle::fetchRow() does only seem to accept object<ResultWrapper>|object<ORAResult>, 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...
842
		$this->mInsertId = $row[0];
843
844
		return $this->mInsertId;
845
	}
846
847
	/**
848
	 * Return sequence_name if table has a sequence
849
	 *
850
	 * @param string $table
851
	 * @return bool
852
	 */
853
	private function getSequenceData( $table ) {
854
		if ( $this->sequenceData == null ) {
855
			$result = $this->doQuery( "SELECT lower(asq.sequence_name),
856
				lower(atc.table_name),
857
				lower(atc.column_name)
858
			FROM all_sequences asq, all_tab_columns atc
859
			WHERE decode(
860
					atc.table_name,
861
					'{$this->mTablePrefix}MWUSER',
862
					'{$this->mTablePrefix}USER',
863
					atc.table_name
864
				) || '_' ||
865
				atc.column_name || '_SEQ' = '{$this->mTablePrefix}' || asq.sequence_name
866
				AND asq.sequence_owner = upper('{$this->mDBname}')
867
				AND atc.owner = upper('{$this->mDBname}')" );
868
869
			while ( ( $row = $result->fetchRow() ) !== false ) {
870
				$this->sequenceData[$row[1]] = [
871
					'sequence' => $row[0],
872
					'column' => $row[2]
873
				];
874
			}
875
		}
876
		$table = strtolower( $this->removeIdentifierQuotes( $this->tableName( $table ) ) );
877
878
		return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false;
879
	}
880
881
	/**
882
	 * Returns the size of a text field, or -1 for "unlimited"
883
	 *
884
	 * @param string $table
885
	 * @param string $field
886
	 * @return mixed
887
	 */
888
	function textFieldSize( $table, $field ) {
889
		$fieldInfoData = $this->fieldInfo( $table, $field );
890
891
		return $fieldInfoData->maxLength();
892
	}
893
894
	function limitResult( $sql, $limit, $offset = false ) {
895
		if ( $offset === false ) {
896
			$offset = 0;
897
		}
898
899
		return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < (1 + $limit + $offset)";
900
	}
901
902
	function encodeBlob( $b ) {
903
		return new Blob( $b );
904
	}
905
906
	function decodeBlob( $b ) {
907
		if ( $b instanceof Blob ) {
908
			$b = $b->fetch();
909
		}
910
911
		return $b;
912
	}
913
914
	function unionQueries( $sqls, $all ) {
915
		$glue = ' UNION ALL ';
916
917
		return 'SELECT * ' . ( $all ? '' : '/* UNION_UNIQUE */ ' ) .
918
			'FROM (' . implode( $glue, $sqls ) . ')';
919
	}
920
921
	function wasDeadlock() {
922
		return $this->lastErrno() == 'OCI-00060';
923
	}
924
925
	function duplicateTableStructure( $oldName, $newName, $temporary = false,
926
		$fname = __METHOD__
927
	) {
928
		$temporary = $temporary ? 'TRUE' : 'FALSE';
929
930
		$newName = strtoupper( $newName );
931
		$oldName = strtoupper( $oldName );
932
933
		$tabName = substr( $newName, strlen( $this->mTablePrefix ) );
934
		$oldPrefix = substr( $oldName, 0, strlen( $oldName ) - strlen( $tabName ) );
935
		$newPrefix = strtoupper( $this->mTablePrefix );
936
937
		return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', " .
938
			"'$oldPrefix', '$newPrefix', $temporary ); END;" );
939
	}
940
941
	function listTables( $prefix = null, $fname = __METHOD__ ) {
942
		$listWhere = '';
943
		if ( !empty( $prefix ) ) {
944
			$listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
945
		}
946
947
		$owner = strtoupper( $this->mDBname );
948
		$result = $this->doQuery( "SELECT table_name FROM all_tables " .
949
			"WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" );
950
951
		// dirty code ... i know
952
		$endArray = [];
953
		$endArray[] = strtoupper( $prefix . 'MWUSER' );
954
		$endArray[] = strtoupper( $prefix . 'PAGE' );
955
		$endArray[] = strtoupper( $prefix . 'IMAGE' );
956
		$fixedOrderTabs = $endArray;
957
		while ( ( $row = $result->fetchRow() ) !== false ) {
958
			if ( !in_array( $row['table_name'], $fixedOrderTabs ) ) {
959
				$endArray[] = $row['table_name'];
960
			}
961
		}
962
963
		return $endArray;
964
	}
965
966
	public function dropTable( $tableName, $fName = __METHOD__ ) {
967
		$tableName = $this->tableName( $tableName );
968
		if ( !$this->tableExists( $tableName ) ) {
969
			return false;
970
		}
971
972
		return $this->doQuery( "DROP TABLE $tableName CASCADE CONSTRAINTS PURGE" );
973
	}
974
975
	function timestamp( $ts = 0 ) {
976
		return wfTimestamp( TS_ORACLE, $ts );
977
	}
978
979
	/**
980
	 * Return aggregated value function call
981
	 *
982
	 * @param array $valuedata
983
	 * @param string $valuename
984
	 * @return mixed
985
	 */
986
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
987
		return $valuedata;
988
	}
989
990
	/**
991
	 * @return string Wikitext of a link to the server software's web site
992
	 */
993
	public function getSoftwareLink() {
994
		return '[{{int:version-db-oracle-url}} Oracle]';
995
	}
996
997
	/**
998
	 * @return string Version information from the database
999
	 */
1000
	function getServerVersion() {
1001
		// better version number, fallback on driver
1002
		$rset = $this->doQuery(
1003
			'SELECT version FROM product_component_version ' .
1004
				'WHERE UPPER(product) LIKE \'ORACLE DATABASE%\''
1005
		);
1006
		$row = $rset->fetchRow();
1007
		if ( !$row ) {
1008
			return oci_server_version( $this->mConn );
1009
		}
1010
1011
		return $row['version'];
1012
	}
1013
1014
	/**
1015
	 * Query whether a given index exists
1016
	 * @param string $table
1017
	 * @param string $index
1018
	 * @param string $fname
1019
	 * @return bool
1020
	 */
1021
	function indexExists( $table, $index, $fname = __METHOD__ ) {
1022
		$table = $this->tableName( $table );
1023
		$table = strtoupper( $this->removeIdentifierQuotes( $table ) );
1024
		$index = strtoupper( $index );
1025
		$owner = strtoupper( $this->mDBname );
1026
		$sql = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'";
1027
		$res = $this->doQuery( $sql );
1028
		if ( $res ) {
1029
			$count = $res->numRows();
1030
			$res->free();
1031
		} else {
1032
			$count = 0;
1033
		}
1034
1035
		return $count != 0;
1036
	}
1037
1038
	/**
1039
	 * Query whether a given table exists (in the given schema, or the default mw one if not given)
1040
	 * @param string $table
1041
	 * @param string $fname
1042
	 * @return bool
1043
	 */
1044
	function tableExists( $table, $fname = __METHOD__ ) {
1045
		$table = $this->tableName( $table );
1046
		$table = $this->addQuotes( strtoupper( $this->removeIdentifierQuotes( $table ) ) );
1047
		$owner = $this->addQuotes( strtoupper( $this->mDBname ) );
1048
		$sql = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
1049
		$res = $this->doQuery( $sql );
1050
		if ( $res && $res->numRows() > 0 ) {
1051
			$exists = true;
1052
		} else {
1053
			$exists = false;
1054
		}
1055
1056
		$res->free();
1057
1058
		return $exists;
1059
	}
1060
1061
	/**
1062
	 * Function translates mysql_fetch_field() functionality on ORACLE.
1063
	 * Caching is present for reducing query time.
1064
	 * For internal calls. Use fieldInfo for normal usage.
1065
	 * Returns false if the field doesn't exist
1066
	 *
1067
	 * @param array|string $table
1068
	 * @param string $field
1069
	 * @return ORAField|ORAResult
1070
	 */
1071
	private function fieldInfoMulti( $table, $field ) {
1072
		$field = strtoupper( $field );
1073
		if ( is_array( $table ) ) {
1074
			$table = array_map( [ &$this, 'tableNameInternal' ], $table );
1075
			$tableWhere = 'IN (';
1076
			foreach ( $table as &$singleTable ) {
1077
				$singleTable = $this->removeIdentifierQuotes( $singleTable );
1078
				if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) {
1079
					return $this->mFieldInfoCache["$singleTable.$field"];
1080
				}
1081
				$tableWhere .= '\'' . $singleTable . '\',';
1082
			}
1083
			$tableWhere = rtrim( $tableWhere, ',' ) . ')';
1084
		} else {
1085
			$table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) );
1086
			if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) {
1087
				return $this->mFieldInfoCache["$table.$field"];
1088
			}
1089
			$tableWhere = '= \'' . $table . '\'';
1090
		}
1091
1092
		$fieldInfoStmt = oci_parse(
1093
			$this->mConn,
1094
			'SELECT * FROM wiki_field_info_full WHERE table_name ' .
1095
				$tableWhere . ' and column_name = \'' . $field . '\''
1096
		);
1097 View Code Duplication
		if ( oci_execute( $fieldInfoStmt, $this->execFlags() ) === false ) {
1098
			$e = oci_error( $fieldInfoStmt );
1099
			$this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ );
1100
1101
			return false;
1102
		}
1103
		$res = new ORAResult( $this, $fieldInfoStmt );
1104
		if ( $res->numRows() == 0 ) {
1105
			if ( is_array( $table ) ) {
1106
				foreach ( $table as &$singleTable ) {
1107
					$this->mFieldInfoCache["$singleTable.$field"] = false;
1108
				}
1109
			} else {
1110
				$this->mFieldInfoCache["$table.$field"] = false;
1111
			}
1112
			$fieldInfoTemp = null;
1113
		} else {
1114
			$fieldInfoTemp = new ORAField( $res->fetchRow() );
1115
			$table = $fieldInfoTemp->tableName();
1116
			$this->mFieldInfoCache["$table.$field"] = $fieldInfoTemp;
1117
		}
1118
		$res->free();
1119
1120
		return $fieldInfoTemp;
1121
	}
1122
1123
	/**
1124
	 * @throws DBUnexpectedError
1125
	 * @param string $table
1126
	 * @param string $field
1127
	 * @return ORAField
1128
	 */
1129
	function fieldInfo( $table, $field ) {
1130
		if ( is_array( $table ) ) {
1131
			throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' );
1132
		}
1133
1134
		return $this->fieldInfoMulti( $table, $field );
1135
	}
1136
1137
	protected function doBegin( $fname = __METHOD__ ) {
1138
		$this->mTrxLevel = 1;
1139
		$this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
1140
	}
1141
1142
	protected function doCommit( $fname = __METHOD__ ) {
1143
		if ( $this->mTrxLevel ) {
1144
			$ret = oci_commit( $this->mConn );
1145
			if ( !$ret ) {
1146
				throw new DBUnexpectedError( $this, $this->lastError() );
1147
			}
1148
			$this->mTrxLevel = 0;
1149
			$this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
1150
		}
1151
	}
1152
1153
	protected function doRollback( $fname = __METHOD__ ) {
1154
		if ( $this->mTrxLevel ) {
1155
			oci_rollback( $this->mConn );
1156
			$this->mTrxLevel = 0;
1157
			$this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' );
1158
		}
1159
	}
1160
1161
	/**
1162
	 * defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}';
1163
	 *
1164
	 * @param resource $fp
1165
	 * @param bool|string $lineCallback
1166
	 * @param bool|callable $resultCallback
1167
	 * @param string $fname
1168
	 * @param bool|callable $inputCallback
1169
	 * @return bool|string
1170
	 */
1171
	function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
1172
		$fname = __METHOD__, $inputCallback = false ) {
1173
		$cmd = '';
1174
		$done = false;
1175
		$dollarquote = false;
1176
1177
		$replacements = [];
1178
1179
		while ( !feof( $fp ) ) {
1180
			if ( $lineCallback ) {
1181
				call_user_func( $lineCallback );
1182
			}
1183
			$line = trim( fgets( $fp, 1024 ) );
1184
			$sl = strlen( $line ) - 1;
1185
1186
			if ( $sl < 0 ) {
1187
				continue;
1188
			}
1189
			if ( '-' == $line[0] && '-' == $line[1] ) {
1190
				continue;
1191
			}
1192
1193
			// Allow dollar quoting for function declarations
1194
			if ( substr( $line, 0, 8 ) == '/*$mw$*/' ) {
1195
				if ( $dollarquote ) {
1196
					$dollarquote = false;
1197
					$line = str_replace( '/*$mw$*/', '', $line ); // remove dollarquotes
1198
					$done = true;
1199
				} else {
1200
					$dollarquote = true;
1201
				}
1202
			} elseif ( !$dollarquote ) {
1203
				if ( ';' == $line[$sl] && ( $sl < 2 || ';' != $line[$sl - 1] ) ) {
1204
					$done = true;
1205
					$line = substr( $line, 0, $sl );
1206
				}
1207
			}
1208
1209
			if ( $cmd != '' ) {
1210
				$cmd .= ' ';
1211
			}
1212
			$cmd .= "$line\n";
1213
1214
			if ( $done ) {
1215
				$cmd = str_replace( ';;', ";", $cmd );
1216
				if ( strtolower( substr( $cmd, 0, 6 ) ) == 'define' ) {
1217
					if ( preg_match( '/^define\s*([^\s=]*)\s*=\s*\'\{\$([^\}]*)\}\'/', $cmd, $defines ) ) {
1218
						$replacements[$defines[2]] = $defines[1];
1219
					}
1220
				} else {
1221
					foreach ( $replacements as $mwVar => $scVar ) {
1222
						$cmd = str_replace( '&' . $scVar . '.', '`{$' . $mwVar . '}`', $cmd );
1223
					}
1224
1225
					$cmd = $this->replaceVars( $cmd );
1226
					if ( $inputCallback ) {
1227
						call_user_func( $inputCallback, $cmd );
1228
					}
1229
					$res = $this->doQuery( $cmd );
1230
					if ( $resultCallback ) {
1231
						call_user_func( $resultCallback, $res, $this );
1232
					}
1233
1234
					if ( false === $res ) {
1235
						$err = $this->lastError();
1236
1237
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
1238
					}
1239
				}
1240
1241
				$cmd = '';
1242
				$done = false;
1243
			}
1244
		}
1245
1246
		return true;
1247
	}
1248
1249
	function selectDB( $db ) {
1250
		$this->mDBname = $db;
1251
		if ( $db == null || $db == $this->mUser ) {
1252
			return true;
1253
		}
1254
		$sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper( $db );
1255
		$stmt = oci_parse( $this->mConn, $sql );
1256
		MediaWiki\suppressWarnings();
1257
		$success = oci_execute( $stmt );
1258
		MediaWiki\restoreWarnings();
1259
		if ( !$success ) {
1260
			$e = oci_error( $stmt );
1261
			if ( $e['code'] != '1435' ) {
1262
				$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
1263
			}
1264
1265
			return false;
1266
		}
1267
1268
		return true;
1269
	}
1270
1271
	function strencode( $s ) {
1272
		return str_replace( "'", "''", $s );
1273
	}
1274
1275
	function addQuotes( $s ) {
1276
		global $wgContLang;
1277
		if ( isset( $wgContLang->mLoaded ) && $wgContLang->mLoaded ) {
1278
			$s = $wgContLang->checkTitleEncoding( $s );
1279
		}
1280
1281
		return "'" . $this->strencode( $s ) . "'";
1282
	}
1283
1284
	public function addIdentifierQuotes( $s ) {
1285
		if ( !$this->getFlag( DBO_DDLMODE ) ) {
1286
			$s = '/*Q*/' . $s;
1287
		}
1288
1289
		return $s;
1290
	}
1291
1292
	public function removeIdentifierQuotes( $s ) {
1293
		return strpos( $s, '/*Q*/' ) === false ? $s : substr( $s, 5 );
1294
	}
1295
1296
	public function isQuotedIdentifier( $s ) {
1297
		return strpos( $s, '/*Q*/' ) !== false;
1298
	}
1299
1300
	private function wrapFieldForWhere( $table, &$col, &$val ) {
1301
		global $wgContLang;
1302
1303
		$col_info = $this->fieldInfoMulti( $table, $col );
1304
		$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
0 ignored issues
show
Bug introduced by
The method type does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1305
		if ( $col_type == 'CLOB' ) {
1306
			$col = 'TO_CHAR(' . $col . ')';
1307
			$val = $wgContLang->checkTitleEncoding( $val );
1308
		} elseif ( $col_type == 'VARCHAR2' ) {
1309
			$val = $wgContLang->checkTitleEncoding( $val );
1310
		}
1311
	}
1312
1313
	private function wrapConditionsForWhere( $table, $conds, $parentCol = null ) {
1314
		$conds2 = [];
1315
		foreach ( $conds as $col => $val ) {
1316
			if ( is_array( $val ) ) {
1317
				$conds2[$col] = $this->wrapConditionsForWhere( $table, $val, $col );
1318
			} else {
1319
				if ( is_numeric( $col ) && $parentCol != null ) {
1320
					$this->wrapFieldForWhere( $table, $parentCol, $val );
1321
				} else {
1322
					$this->wrapFieldForWhere( $table, $col, $val );
1323
				}
1324
				$conds2[$col] = $val;
1325
			}
1326
		}
1327
1328
		return $conds2;
1329
	}
1330
1331
	function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1332
		$options = [], $join_conds = []
1333
	) {
1334
		if ( is_array( $conds ) ) {
1335
			$conds = $this->wrapConditionsForWhere( $table, $conds );
1336
		}
1337
1338
		return parent::selectRow( $table, $vars, $conds, $fname, $options, $join_conds );
1339
	}
1340
1341
	/**
1342
	 * Returns an optional USE INDEX clause to go after the table, and a
1343
	 * string to go at the end of the query
1344
	 *
1345
	 * @param array $options An associative array of options to be turned into
1346
	 *   an SQL query, valid keys are listed in the function.
1347
	 * @return array
1348
	 */
1349
	function makeSelectOptions( $options ) {
1350
		$preLimitTail = $postLimitTail = '';
1351
		$startOpts = '';
1352
1353
		$noKeyOptions = [];
1354
		foreach ( $options as $key => $option ) {
1355
			if ( is_numeric( $key ) ) {
1356
				$noKeyOptions[$option] = true;
1357
			}
1358
		}
1359
1360
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1361
1362
		$preLimitTail .= $this->makeOrderBy( $options );
1363
1364
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1365
			$postLimitTail .= ' FOR UPDATE';
1366
		}
1367
1368
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1369
			$startOpts .= 'DISTINCT';
1370
		}
1371
1372 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && !is_array( $options['USE INDEX'] ) ) {
1373
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1374
		} else {
1375
			$useIndex = '';
1376
		}
1377
1378
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1379
	}
1380
1381
	public function delete( $table, $conds, $fname = __METHOD__ ) {
1382
		if ( is_array( $conds ) ) {
1383
			$conds = $this->wrapConditionsForWhere( $table, $conds );
1384
		}
1385
		// a hack for deleting pages, users and images (which have non-nullable FKs)
1386
		// all deletions on these tables have transactions so final failure rollbacks these updates
1387
		$table = $this->tableName( $table );
1388
		if ( $table == $this->tableName( 'user' ) ) {
1389
			$this->update( 'archive', [ 'ar_user' => 0 ],
1390
				[ 'ar_user' => $conds['user_id'] ], $fname );
1391
			$this->update( 'ipblocks', [ 'ipb_user' => 0 ],
1392
				[ 'ipb_user' => $conds['user_id'] ], $fname );
1393
			$this->update( 'image', [ 'img_user' => 0 ],
1394
				[ 'img_user' => $conds['user_id'] ], $fname );
1395
			$this->update( 'oldimage', [ 'oi_user' => 0 ],
1396
				[ 'oi_user' => $conds['user_id'] ], $fname );
1397
			$this->update( 'filearchive', [ 'fa_deleted_user' => 0 ],
1398
				[ 'fa_deleted_user' => $conds['user_id'] ], $fname );
1399
			$this->update( 'filearchive', [ 'fa_user' => 0 ],
1400
				[ 'fa_user' => $conds['user_id'] ], $fname );
1401
			$this->update( 'uploadstash', [ 'us_user' => 0 ],
1402
				[ 'us_user' => $conds['user_id'] ], $fname );
1403
			$this->update( 'recentchanges', [ 'rc_user' => 0 ],
1404
				[ 'rc_user' => $conds['user_id'] ], $fname );
1405
			$this->update( 'logging', [ 'log_user' => 0 ],
1406
				[ 'log_user' => $conds['user_id'] ], $fname );
1407
		} elseif ( $table == $this->tableName( 'image' ) ) {
1408
			$this->update( 'oldimage', [ 'oi_name' => 0 ],
1409
				[ 'oi_name' => $conds['img_name'] ], $fname );
1410
		}
1411
1412
		return parent::delete( $table, $conds, $fname );
1413
	}
1414
1415
	/**
1416
	 * @param string $table
1417
	 * @param array $values
1418
	 * @param array $conds
1419
	 * @param string $fname
1420
	 * @param array $options
1421
	 * @return bool
1422
	 * @throws DBUnexpectedError
1423
	 */
1424
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1425
		global $wgContLang;
1426
1427
		$table = $this->tableName( $table );
1428
		$opts = $this->makeUpdateOptions( $options );
1429
		$sql = "UPDATE $opts $table SET ";
1430
1431
		$first = true;
1432
		foreach ( $values as $col => &$val ) {
1433
			$sqlSet = $this->fieldBindStatement( $table, $col, $val, true );
1434
1435
			if ( !$first ) {
1436
				$sqlSet = ', ' . $sqlSet;
1437
			} else {
1438
				$first = false;
1439
			}
1440
			$sql .= $sqlSet;
1441
		}
1442
1443
		if ( $conds !== [] && $conds !== '*' ) {
1444
			$conds = $this->wrapConditionsForWhere( $table, $conds );
1445
			$sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
1446
		}
1447
1448
		$this->mLastResult = $stmt = oci_parse( $this->mConn, $sql );
1449 View Code Duplication
		if ( $stmt === false ) {
1450
			$e = oci_error( $this->mConn );
1451
			$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
1452
1453
			return false;
1454
		}
1455
		foreach ( $values as $col => &$val ) {
1456
			$col_info = $this->fieldInfoMulti( $table, $col );
1457
			$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
0 ignored issues
show
Bug introduced by
The method type does only exist in ORAField, but not in ORAResult.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1458
1459
			if ( $val === null ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1460
				// do nothing ... null was inserted in statement creation
1461
			} elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) {
1462
				if ( is_object( $val ) ) {
1463
					$val = $val->getData();
1464
				}
1465
1466
				if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
1467
					$val = '31-12-2030 12:00:00.000000';
1468
				}
1469
1470
				$val = ( $wgContLang != null ) ? $wgContLang->checkTitleEncoding( $val ) : $val;
1471
				if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) {
1472
					$e = oci_error( $stmt );
1473
					$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
1474
1475
					return false;
1476
				}
1477 View Code Duplication
			} else {
1478
				/** @var OCI_Lob[] $lob */
1479
				$lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB );
0 ignored issues
show
Coding Style Comprehensibility introduced by
$lob was never initialized. Although not strictly required by PHP, it is generally a good practice to add $lob = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1480
				if ( $lob[$col] === false ) {
1481
					$e = oci_error( $stmt );
1482
					throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
1483
				}
1484
1485
				if ( is_object( $val ) ) {
1486
					$val = $val->getData();
1487
				}
1488
1489
				if ( $col_type == 'BLOB' ) {
1490
					$lob[$col]->writeTemporary( $val );
1491
					oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, SQLT_BLOB );
1492
				} else {
1493
					$lob[$col]->writeTemporary( $val );
1494
					oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_CLOB );
1495
				}
1496
			}
1497
		}
1498
1499
		MediaWiki\suppressWarnings();
1500
1501 View Code Duplication
		if ( oci_execute( $stmt, $this->execFlags() ) === false ) {
1502
			$e = oci_error( $stmt );
1503
			if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) {
1504
				$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
1505
1506
				return false;
1507
			} else {
1508
				$this->mAffectedRows = oci_num_rows( $stmt );
1509
			}
1510
		} else {
1511
			$this->mAffectedRows = oci_num_rows( $stmt );
1512
		}
1513
1514
		MediaWiki\restoreWarnings();
1515
1516
		if ( isset( $lob ) ) {
1517
			foreach ( $lob as $lob_v ) {
1518
				$lob_v->free();
1519
			}
1520
		}
1521
1522
		if ( !$this->mTrxLevel ) {
1523
			oci_commit( $this->mConn );
1524
		}
1525
1526
		return oci_free_statement( $stmt );
1527
	}
1528
1529
	function bitNot( $field ) {
1530
		// expecting bit-fields smaller than 4bytes
1531
		return 'BITNOT(' . $field . ')';
1532
	}
1533
1534
	function bitAnd( $fieldLeft, $fieldRight ) {
1535
		return 'BITAND(' . $fieldLeft . ', ' . $fieldRight . ')';
1536
	}
1537
1538
	function bitOr( $fieldLeft, $fieldRight ) {
1539
		return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')';
1540
	}
1541
1542
	function getDBname() {
1543
		return $this->mDBname;
1544
	}
1545
1546
	function getServer() {
1547
		return $this->mServer;
1548
	}
1549
1550 View Code Duplication
	public function buildGroupConcatField(
1551
		$delim, $table, $field, $conds = '', $join_conds = []
1552
	) {
1553
		$fld = "LISTAGG($field," . $this->addQuotes( $delim ) . ") WITHIN GROUP (ORDER BY $field)";
1554
1555
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1556
	}
1557
1558
	/**
1559
	 * @param string $field Field or column to cast
1560
	 * @return string
1561
	 * @since 1.28
1562
	 */
1563
	public function buildStringCast( $field ) {
1564
		return 'CAST ( ' . $field . ' AS VARCHAR2 )';
1565
	}
1566
1567
	public function getSearchEngine() {
1568
		return 'SearchOracle';
1569
	}
1570
1571
	public function getInfinity() {
1572
		return '31-12-2030 12:00:00.000000';
1573
	}
1574
}
1575