mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
3ee8f505b7
The main benefit of this is so that authors who make use of .editorconfig don't end up with whitespace changes in their PRs. Spaces vs. tabs has been left alone, although that could do with a tidy-up in SS4 after the switch to PSR-1/2. The command used was this: for match in '*.ss' '*.css' '*.scss' '*.html' '*.yml' '*.php' '*.js' '*.csv' '*.inc' '*.php5'; do find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" -exec sed -E -i '' 's/[[:space:]]+$//' {} \+ find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" | xargs perl -pi -e 's/ +$//' done
345 lines
10 KiB
PHP
345 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* MySQL connector class.
|
|
*
|
|
* Supported indexes for {@link requireTable()}:
|
|
*
|
|
* @package framework
|
|
* @subpackage model
|
|
*/
|
|
class MySQLDatabase extends SS_Database {
|
|
|
|
/**
|
|
* Default connection charset (may be overridden in $databaseConfig)
|
|
*
|
|
* @config
|
|
* @var String
|
|
*/
|
|
private static $connection_charset = null;
|
|
|
|
public function connect($parameters) {
|
|
// Ensure that driver is available (required by PDO)
|
|
if(empty($parameters['driver'])) {
|
|
$parameters['driver'] = $this->getDatabaseServer();
|
|
}
|
|
|
|
// Set charset
|
|
if( empty($parameters['charset'])
|
|
&& ($charset = Config::inst()->get('MySQLDatabase', 'connection_charset'))
|
|
) {
|
|
$parameters['charset'] = $charset;
|
|
}
|
|
|
|
// Notify connector of parameters
|
|
$this->connector->connect($parameters);
|
|
|
|
// This is important!
|
|
$this->setSQLMode('ANSI');
|
|
|
|
if (isset($parameters['timezone'])) {
|
|
$this->selectTimezone($parameters['timezone']);
|
|
}
|
|
|
|
// SS_Database subclass maintains responsibility for selecting database
|
|
// once connected in order to correctly handle schema queries about
|
|
// existence of database, error handling at the correct level, etc
|
|
if (!empty($parameters['database'])) {
|
|
$this->selectDatabase($parameters['database'], false, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the SQL mode
|
|
*
|
|
* @param string $mode Connection mode
|
|
*/
|
|
public function setSQLMode($mode) {
|
|
if (empty($mode)) return;
|
|
$this->preparedQuery("SET sql_mode = ?", array($mode));
|
|
}
|
|
|
|
/**
|
|
* Sets the system timezone for the database connection
|
|
*
|
|
* @param string $timezone
|
|
*/
|
|
public function selectTimezone($timezone) {
|
|
if (empty($timezone)) return;
|
|
$this->preparedQuery("SET SESSION time_zone = ?", array($timezone));
|
|
}
|
|
|
|
public function supportsCollations() {
|
|
return true;
|
|
}
|
|
|
|
public function supportsTimezoneOverride() {
|
|
return true;
|
|
}
|
|
|
|
public function getDatabaseServer() {
|
|
return "mysql";
|
|
}
|
|
|
|
/**
|
|
* The core search engine, used by this class and its subclasses to do fun stuff.
|
|
* Searches both SiteTree and File.
|
|
*
|
|
* @param string $keywords Keywords as a string.
|
|
*/
|
|
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
|
|
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
|
|
) {
|
|
if (!class_exists('SiteTree'))
|
|
throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
|
|
if (!class_exists('File'))
|
|
throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
|
|
|
|
$keywords = $this->escapeString($keywords);
|
|
$htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
|
|
|
|
$extraFilters = array('SiteTree' => '', 'File' => '');
|
|
|
|
if ($booleanSearch) $boolean = "IN BOOLEAN MODE";
|
|
|
|
if ($extraFilter) {
|
|
$extraFilters['SiteTree'] = " AND $extraFilter";
|
|
|
|
if ($alternativeFileFilter)
|
|
$extraFilters['File'] = " AND $alternativeFileFilter";
|
|
else $extraFilters['File'] = $extraFilters['SiteTree'];
|
|
}
|
|
|
|
// Always ensure that only pages with ShowInSearch = 1 can be searched
|
|
$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
|
|
|
|
// File.ShowInSearch was added later, keep the database driver backwards compatible
|
|
// by checking for its existence first
|
|
$fields = $this->fieldList('File');
|
|
if (array_key_exists('ShowInSearch', $fields))
|
|
$extraFilters['File'] .= " AND ShowInSearch <> 0";
|
|
|
|
$limit = $start . ", " . (int) $pageLength;
|
|
|
|
$notMatch = $invertedMatch
|
|
? "NOT "
|
|
: "";
|
|
if ($keywords) {
|
|
$match['SiteTree'] = "
|
|
MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$keywords' $boolean)
|
|
+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityKeywords' $boolean)
|
|
";
|
|
$match['File'] = "MATCH (Name, Title) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
|
|
|
|
// We make the relevance search by converting a boolean mode search into a normal one
|
|
$relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords);
|
|
$htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords);
|
|
$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaDescription) "
|
|
. "AGAINST ('$relevanceKeywords') "
|
|
. "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
|
|
$relevance['File'] = "MATCH (Name, Title) AGAINST ('$relevanceKeywords')";
|
|
} else {
|
|
$relevance['SiteTree'] = $relevance['File'] = 1;
|
|
$match['SiteTree'] = $match['File'] = "1 = 1";
|
|
}
|
|
|
|
// Generate initial DataLists and base table names
|
|
$lists = array();
|
|
$baseClasses = array('SiteTree' => '', 'File' => '');
|
|
foreach ($classesToSearch as $class) {
|
|
$lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "");
|
|
$baseClasses[$class] = '"' . $class . '"';
|
|
}
|
|
|
|
// Make column selection lists
|
|
$select = array(
|
|
'SiteTree' => array(
|
|
"ClassName", "$baseClasses[SiteTree].\"ID\"", "ParentID",
|
|
"Title", "MenuTitle", "URLSegment", "Content",
|
|
"LastEdited", "Created",
|
|
"Name" => "_utf8''",
|
|
"Relevance" => $relevance['SiteTree'], "CanViewType"
|
|
),
|
|
'File' => array(
|
|
"ClassName", "$baseClasses[File].\"ID\"", "ParentID" => "_utf8''",
|
|
"Title", "MenuTitle" => "_utf8''", "URLSegment" => "_utf8''", "Content" => "_utf8''",
|
|
"LastEdited", "Created",
|
|
"Name",
|
|
"Relevance" => $relevance['File'], "CanViewType" => "NULL"
|
|
),
|
|
);
|
|
|
|
// Process and combine queries
|
|
$querySQLs = array();
|
|
$queryParameters = array();
|
|
$totalCount = 0;
|
|
foreach ($lists as $class => $list) {
|
|
$query = $list->dataQuery()->query();
|
|
|
|
// There's no need to do all that joining
|
|
$query->setFrom(array(str_replace(array('"', '`'), '', $baseClasses[$class]) => $baseClasses[$class]));
|
|
$query->setSelect($select[$class]);
|
|
$query->setOrderBy(array());
|
|
|
|
$querySQLs[] = $query->sql($parameters);
|
|
$queryParameters = array_merge($queryParameters, $parameters);
|
|
|
|
$totalCount += $query->unlimitedRowCount();
|
|
}
|
|
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
|
|
|
|
// Get records
|
|
$records = $this->preparedQuery($fullQuery, $queryParameters);
|
|
|
|
$objects = array();
|
|
|
|
foreach ($records as $record) {
|
|
$objects[] = new $record['ClassName']($record);
|
|
}
|
|
|
|
$list = new PaginatedList(new ArrayList($objects));
|
|
$list->setPageStart($start);
|
|
$list->setPageLength($pageLength);
|
|
$list->setTotalItems($totalCount);
|
|
|
|
// The list has already been limited by the query above
|
|
$list->setLimitItems(false);
|
|
|
|
return $list;
|
|
}
|
|
|
|
public function supportsTransactions() {
|
|
return true;
|
|
}
|
|
|
|
public function transactionStart($transactionMode = false, $sessionCharacteristics = false) {
|
|
// This sets the isolation level for the NEXT transaction, not the current one.
|
|
if ($transactionMode) {
|
|
$this->query('SET TRANSACTION ' . $transactionMode);
|
|
}
|
|
|
|
$this->query('START TRANSACTION');
|
|
|
|
if ($sessionCharacteristics) {
|
|
$this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics);
|
|
}
|
|
}
|
|
|
|
public function transactionSavepoint($savepoint) {
|
|
$this->query("SAVEPOINT $savepoint");
|
|
}
|
|
|
|
public function transactionRollback($savepoint = false) {
|
|
if ($savepoint) {
|
|
$this->query('ROLLBACK TO ' . $savepoint);
|
|
} else {
|
|
$this->query('ROLLBACK');
|
|
}
|
|
}
|
|
|
|
public function transactionEnd($chain = false) {
|
|
$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN');
|
|
}
|
|
|
|
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
|
|
$parameterised = false
|
|
) {
|
|
if ($exact && $caseSensitive === null) {
|
|
$comp = ($negate) ? '!=' : '=';
|
|
} else {
|
|
$comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
|
|
if ($negate) $comp = 'NOT ' . $comp;
|
|
}
|
|
|
|
if($parameterised) {
|
|
return sprintf("%s %s ?", $field, $comp);
|
|
} else {
|
|
return sprintf("%s %s '%s'", $field, $comp, $value);
|
|
}
|
|
}
|
|
|
|
public function formattedDatetimeClause($date, $format) {
|
|
preg_match_all('/%(.)/', $format, $matches);
|
|
foreach ($matches[1] as $match)
|
|
if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
|
|
user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
|
}
|
|
|
|
if (preg_match('/^now$/i', $date)) {
|
|
$date = "NOW()";
|
|
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
|
$date = "'$date'";
|
|
}
|
|
|
|
if ($format == '%U') return "UNIX_TIMESTAMP($date)";
|
|
|
|
return "DATE_FORMAT($date, '$format')";
|
|
}
|
|
|
|
public function datetimeIntervalClause($date, $interval) {
|
|
$interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
|
|
|
|
if (preg_match('/^now$/i', $date)) {
|
|
$date = "NOW()";
|
|
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
|
$date = "'$date'";
|
|
}
|
|
|
|
return "$date + INTERVAL $interval";
|
|
}
|
|
|
|
public function datetimeDifferenceClause($date1, $date2) {
|
|
// First date format
|
|
if (preg_match('/^now$/i', $date1)) {
|
|
$date1 = "NOW()";
|
|
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
|
$date1 = "'$date1'";
|
|
}
|
|
// Second date format
|
|
if (preg_match('/^now$/i', $date2)) {
|
|
$date2 = "NOW()";
|
|
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
|
|
$date2 = "'$date2'";
|
|
}
|
|
|
|
return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
|
|
}
|
|
|
|
public function supportsLocks() {
|
|
return true;
|
|
}
|
|
|
|
public function canLock($name) {
|
|
$id = $this->getLockIdentifier($name);
|
|
return (bool) $this->query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
|
|
}
|
|
|
|
public function getLock($name, $timeout = 5) {
|
|
$id = $this->getLockIdentifier($name);
|
|
|
|
// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
|
|
// in contrast to PostgreSQL and SQL Server who stack the locks.
|
|
return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
|
|
}
|
|
|
|
public function releaseLock($name) {
|
|
$id = $this->getLockIdentifier($name);
|
|
return (bool) $this->query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
|
|
}
|
|
|
|
protected function getLockIdentifier($name) {
|
|
// Prefix with database name
|
|
$dbName = $this->connector->getSelectedDatabase() ;
|
|
return $this->escapeString("{$dbName}_{$name}");
|
|
}
|
|
|
|
public function now() {
|
|
// MySQL uses NOW() to return the current date/time.
|
|
return 'NOW()';
|
|
}
|
|
|
|
public function random() {
|
|
return 'RAND()';
|
|
}
|
|
}
|