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 character set for the MySQL database connection. * * The character set connection should be set to 'utf8' for SilverStripe version 2.4.0 and * later. * * However, sites created before version 2.4.0 should leave this unset or data that isn't 7-bit * safe will be corrupted. As such, the installer comes with this set in mysite/_config.php by * default in versions 2.4.0 and later. * * @deprecated 3.2 Use "MySQLDatabase.connection_charset" config setting instead */ public static function set_connection_charset($charset = 'utf8') { Deprecation::notice('3.1', 'Use "MySQLDatabase.connection_charset" config setting instead'); Config::inst()->update('MySQLDatabase', 'connection_charset', $charset); } /** * 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 (Filename, Title, Content) 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 (Filename, Title, Content) 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", "Filename" => "_utf8''", "Name" => "_utf8''", "Relevance" => $relevance['SiteTree'], "CanViewType" ), 'File' => array( "ClassName", "$baseClasses[File].\"ID\"", "ParentID" => "_utf8''", "Title", "MenuTitle" => "_utf8''", "URLSegment" => "_utf8''", "Content", "LastEdited", "Created", "Filename", "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()'; } }