Completed
Push — 3.7 ( 726fc3...8061e7 )
by Loz
10:04
created

MySQLDatabase::clearTable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * MySQL connector class.
5
 *
6
 * Supported indexes for {@link requireTable()}:
7
 *
8
 * @package framework
9
 * @subpackage model
10
 */
11
class MySQLDatabase extends SS_Database {
12
13
	/**
14
	 * Default connection charset (may be overridden in $databaseConfig)
15
	 *
16
	 * @config
17
	 * @var String
18
	 */
19
	private static $connection_charset = null;
20
21
	public function connect($parameters) {
22
		// Ensure that driver is available (required by PDO)
23
		if(empty($parameters['driver'])) {
24
			$parameters['driver'] = $this->getDatabaseServer();
25
		}
26
27
		// Set charset
28
		if( empty($parameters['charset'])
29
			&& ($charset = Config::inst()->get('MySQLDatabase', 'connection_charset'))
30
		) {
31
			$parameters['charset'] = $charset;
32
		}
33
34
		// Set collation
35
		if( empty($parameters['collation'])
36
			&& ($collation = Config::inst()->get('MySQLDatabase', 'connection_collation'))
37
		) {
38
			$parameters['collation'] = $collation;
39
		}
40
41
		// Notify connector of parameters
42
		$this->connector->connect($parameters);
43
44
		// This is important!
45
		$this->setSQLMode('ANSI');
46
47
		if (isset($parameters['timezone'])) {
48
			$this->selectTimezone($parameters['timezone']);
49
		}
50
51
		// SS_Database subclass maintains responsibility for selecting database
52
		// once connected in order to correctly handle schema queries about
53
		// existence of database, error handling at the correct level, etc
54
		if (!empty($parameters['database'])) {
55
			$this->selectDatabase($parameters['database'], false, false);
56
		}
57
	}
58
59
	/**
60
	 * @deprecated 4.0 Use "MySQLDatabase.connection_charset" config setting instead
61
	 */
62
	public static function set_connection_charset($charset = 'utf8') {
63
		Deprecation::notice('4.0', 'Use "MySQLDatabase.connection_charset" config setting instead');
64
		Config::inst()->update('MySQLDatabase', 'connection_charset', $charset);
65
	}
66
67
	/**
68
	 * Sets the SQL mode
69
	 *
70
	 * @param string $mode Connection mode
71
	 */
72
	public function setSQLMode($mode) {
73
		if (empty($mode)) return;
74
		$this->preparedQuery("SET sql_mode = ?", array($mode));
75
	}
76
77
	/**
78
	 * Sets the system timezone for the database connection
79
	 *
80
	 * @param string $timezone
81
	 */
82
	public function selectTimezone($timezone) {
83
		if (empty($timezone)) return;
84
		$this->preparedQuery("SET SESSION time_zone = ?", array($timezone));
85
	}
86
87
	public function supportsCollations() {
88
		return true;
89
	}
90
91
	public function supportsTimezoneOverride() {
92
		return true;
93
	}
94
95
	public function getDatabaseServer() {
96
		return "mysql";
97
	}
98
99
	/**
100
	 * The core search engine, used by this class and its subclasses to do fun stuff.
101
	 * Searches both SiteTree and File.
102
	 *
103
	 * @param string $keywords Keywords as a string.
104
	 */
105
	public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
106
		$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
107
	) {
108
		if (!class_exists('SiteTree')) {
109
			throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
110
		}
111
		if (!class_exists('File')) {
112
			throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
113
		}
114
		$start = (int)$start;
115
		$pageLength = (int)$pageLength;
116
117
		$keywords = $this->escapeString($keywords);
118
		$htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
119
120
		$extraFilters = array('SiteTree' => '', 'File' => '');
121
122
		if ($booleanSearch) $boolean = "IN BOOLEAN MODE";
123
124
		if ($extraFilter) {
125
			$extraFilters['SiteTree'] = " AND $extraFilter";
126
127
			if ($alternativeFileFilter)
128
					$extraFilters['File'] = " AND $alternativeFileFilter";
129
			else $extraFilters['File'] = $extraFilters['SiteTree'];
130
		}
131
132
		// Always ensure that only pages with ShowInSearch = 1 can be searched
133
		$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
134
135
		// File.ShowInSearch was added later, keep the database driver backwards compatible
136
		// by checking for its existence first
137
		$fields = $this->fieldList('File');
0 ignored issues
show
Deprecated Code introduced by
The method SS_Database::fieldList() has been deprecated with message: since version 4.0 Use DB::field_list instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
138
		if (array_key_exists('ShowInSearch', $fields))
139
				$extraFilters['File'] .= " AND ShowInSearch <> 0";
140
141
		$limit = $start . ", " . $pageLength;
142
143
		$notMatch = $invertedMatch
144
				? "NOT "
145
				: "";
146
		if ($keywords) {
147
			$match['SiteTree'] = "
0 ignored issues
show
Coding Style Comprehensibility introduced by
$match was never initialized. Although not strictly required by PHP, it is generally a good practice to add $match = 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...
148
				MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$keywords' $boolean)
0 ignored issues
show
Bug introduced by
The variable $boolean does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
149
				+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityKeywords' $boolean)
150
			";
151
			$match['File'] = "MATCH (Filename, Title, Content) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
152
153
			// We make the relevance search by converting a boolean mode search into a normal one
154
			$relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords);
155
			$htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords);
156
			$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaDescription) "
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relevance was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relevance = 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...
157
					. "AGAINST ('$relevanceKeywords') "
158
					. "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
159
			$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
160
		} else {
161
			$relevance['SiteTree'] = $relevance['File'] = 1;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$relevance was never initialized. Although not strictly required by PHP, it is generally a good practice to add $relevance = 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...
162
			$match['SiteTree'] = $match['File'] = "1 = 1";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$match was never initialized. Although not strictly required by PHP, it is generally a good practice to add $match = 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...
163
		}
164
165
		// Generate initial DataLists and base table names
166
		$lists = array();
167
		$baseClasses = array('SiteTree' => '', 'File' => '');
168
		foreach ($classesToSearch as $class) {
169
			$lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "");
0 ignored issues
show
Unused Code introduced by
The call to DataList::where() has too many arguments starting with ''.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
170
			$baseClasses[$class] = '"' . $class . '"';
171
		}
172
173
		$charset = Config::inst()->get('MySQLDatabase', 'charset');
174
175
		// Make column selection lists
176
		$select = array(
177
			'SiteTree' => array(
178
				"ClassName", "$baseClasses[SiteTree].\"ID\"", "ParentID",
179
				"Title", "MenuTitle", "URLSegment", "Content",
180
				"LastEdited", "Created",
181
				"Filename" => "_{$charset}''", "Name" => "_{$charset}''",
182
				"Relevance" => $relevance['SiteTree'], "CanViewType"
183
			),
184
			'File' => array(
185
				"ClassName", "$baseClasses[File].\"ID\"", "ParentID",
186
				"Title", "MenuTitle" => "_{$charset}''", "URLSegment" => "_{$charset}''", "Content",
187
				"LastEdited", "Created",
188
				"Filename", "Name",
189
				"Relevance" => $relevance['File'], "CanViewType" => "NULL"
190
			),
191
		);
192
193
		// Process and combine queries
194
		$querySQLs = array();
195
		$queryParameters = array();
196
		$totalCount = 0;
197
		foreach ($lists as $class => $list) {
198
			$query = $list->dataQuery()->query();
199
200
			// There's no need to do all that joining
201
			$query->setFrom(array(str_replace(array('"', '`'), '', $baseClasses[$class]) => $baseClasses[$class]));
202
			$query->setSelect($select[$class]);
203
			$query->setOrderBy(array());
204
205
			$querySQLs[] = $query->sql($parameters);
0 ignored issues
show
Bug introduced by
The variable $parameters does not exist. Did you mean $queryParameters?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
206
			$queryParameters = array_merge($queryParameters, $parameters);
0 ignored issues
show
Bug introduced by
The variable $parameters does not exist. Did you mean $queryParameters?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
207
208
			$totalCount += $query->unlimitedRowCount();
209
		}
210
		$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
211
212
		// Get records
213
		$records = $this->preparedQuery($fullQuery, $queryParameters);
214
215
		$objects = array();
216
217
		foreach ($records as $record) {
218
			$objects[] = new $record['ClassName']($record);
219
		}
220
221
		$list = new PaginatedList(new ArrayList($objects));
222
		$list->setPageStart($start);
223
		$list->setPageLength($pageLength);
224
		$list->setTotalItems($totalCount);
225
226
		// The list has already been limited by the query above
227
		$list->setLimitItems(false);
228
229
		return $list;
230
	}
231
232
	public function supportsTransactions() {
233
		return true;
234
	}
235
236
	public function transactionStart($transactionMode = false, $sessionCharacteristics = false) {
237
		// This sets the isolation level for the NEXT transaction, not the current one.
238
		if ($transactionMode) {
239
			$this->query('SET TRANSACTION ' . $transactionMode);
240
		}
241
242
		$this->query('START TRANSACTION');
243
244
		if ($sessionCharacteristics) {
245
			$this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics);
246
		}
247
	}
248
249
	public function transactionSavepoint($savepoint) {
250
		$this->query("SAVEPOINT $savepoint");
251
	}
252
253
	public function transactionRollback($savepoint = false) {
254
		if ($savepoint) {
255
			$this->query('ROLLBACK TO ' . $savepoint);
256
		} else {
257
			$this->query('ROLLBACK');
258
		}
259
	}
260
261
	public function transactionEnd($chain = false) {
262
		$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN');
263
	}
264
265
	public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
266
		$parameterised = false
267
	) {
268
		if ($exact && $caseSensitive === null) {
269
			$comp = ($negate) ? '!=' : '=';
270
		} else {
271
			$comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
272
			if ($negate) $comp = 'NOT ' . $comp;
273
		}
274
275
		if($parameterised) {
276
			return sprintf("%s %s ?", $field, $comp);
277
		} else {
278
			return sprintf("%s %s '%s'", $field, $comp, $value);
279
		}
280
	}
281
282
	public function formattedDatetimeClause($date, $format) {
283
		preg_match_all('/%(.)/', $format, $matches);
284
		foreach ($matches[1] as $match)
285
			if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
286
				user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
287
			}
288
289
		if (preg_match('/^now$/i', $date)) {
290
			$date = "NOW()";
291
		} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
292
			$date = "'$date'";
293
		}
294
295
		if ($format == '%U') return "UNIX_TIMESTAMP($date)";
296
297
		return "DATE_FORMAT($date, '$format')";
298
	}
299
300
	public function datetimeIntervalClause($date, $interval) {
301
		$interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
302
303
		if (preg_match('/^now$/i', $date)) {
304
			$date = "NOW()";
305
		} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
306
			$date = "'$date'";
307
		}
308
309
		return "$date + INTERVAL $interval";
310
	}
311
312
	public function datetimeDifferenceClause($date1, $date2) {
313
		// First date format
314
		if (preg_match('/^now$/i', $date1)) {
315
			$date1 = "NOW()";
316
		} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
317
			$date1 = "'$date1'";
318
		}
319
		// Second date format
320
		if (preg_match('/^now$/i', $date2)) {
321
			$date2 = "NOW()";
322
		} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
323
			$date2 = "'$date2'";
324
		}
325
326
		return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
327
	}
328
329
	public function supportsLocks() {
330
		return true;
331
	}
332
333
	public function canLock($name) {
334
		$id = $this->getLockIdentifier($name);
335
		return (bool) $this->query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
336
	}
337
338
	public function getLock($name, $timeout = 5) {
339
		$id = $this->getLockIdentifier($name);
340
341
		// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
342
		// in contrast to PostgreSQL and SQL Server who stack the locks.
343
		return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
344
	}
345
346
	public function releaseLock($name) {
347
		$id = $this->getLockIdentifier($name);
348
		return (bool) $this->query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
349
	}
350
351
	protected function getLockIdentifier($name) {
352
		// Prefix with database name
353
		$dbName = $this->connector->getSelectedDatabase() ;
354
		return $this->escapeString("{$dbName}_{$name}");
355
	}
356
357
	public function now() {
358
		// MySQL uses NOW() to return the current date/time.
359
		return 'NOW()';
360
	}
361
362
	public function random() {
363
		return 'RAND()';
364
	}
365
366
	/**
367
	 * Clear all data in a given table
368
	 *
369
	 * @param string $table Name of table
370
	 */
371
	public function clearTable($table) {
372
		$this->query("DELETE FROM \"$table\"");
373
374
		// Check if resetting the auto-increment is needed
375
		$autoIncrement = $this->preparedQuery(
376
			'SELECT "AUTO_INCREMENT" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?',
377
			[ $this->getSelectedDatabase(), $table]
378
		)->value();
379
380
		if ($autoIncrement > 1) {
381
			$this->query("ALTER TABLE \"$table\" AUTO_INCREMENT = 1");
382
		}
383
	}
384
}
385