Merge remote-tracking branch 'origin/3.1' into 3.2

Conflicts:
	admin/code/LeftAndMain.php
	control/injector/SilverStripeServiceConfigurationLocator.php
	core/ClassInfo.php
	filesystem/File.php
	model/DataObject.php
	model/DataQuery.php
	search/filters/FulltextFilter.php
	search/filters/SearchFilter.php
	tests/core/ClassInfoTest.php
	tests/filesystem/FileTest.php
	tests/model/DataListTest.php
This commit is contained in:
Damian Mooyman 2015-07-31 11:38:18 +12:00
commit 7ee444e08a
25 changed files with 483 additions and 121 deletions

View File

@ -7,10 +7,6 @@ addons:
packages:
- tidy
cache:
directories:
- $HOME/.composer/cache
php:
- 5.4

View File

@ -76,7 +76,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @config
* @var string
*/
private static $help_link = 'http://userhelp.silverstripe.org/en/3.2/';
private static $help_link = '//userhelp.silverstripe.org/framework/en/3.2';
/**
* @var array
@ -1618,7 +1618,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @config
* @var String
*/
private static $application_link = 'http://www.silverstripe.org/';
private static $application_link = '//www.silverstripe.org/';
/**
* Sets the href for the anchor on the Silverstripe logo in the menu

View File

@ -135,7 +135,7 @@ class LeftAndMainTest extends FunctionalTest {
$link = $menuItem->Link;
// don't test external links
if(preg_match('/^https?:\/\//',$link)) continue;
if(preg_match('/^(https?:)?\/\//',$link)) continue;
$response = $this->get($link);

View File

@ -11,21 +11,22 @@ class SilverStripeServiceConfigurationLocator extends ServiceConfigurationLocato
/**
* List of Injector configurations cached from Config in class => config format.
* If any config is false, this denotes that this class and all its parents
* If any config is false, this denotes that this class and all its parents
* have no configuration specified.
*
*
* @var array
*/
protected $configs = array();
public function locateConfigFor($name) {
// Check direct or cached result
$config = $this->configFor($name);
if($config !== null) return $config;
// do parent lookup if it's a class
if (class_exists($name)) {
$parents = array_reverse(array_keys(ClassInfo::ancestry($name)));
$parents = array_reverse(array_values(ClassInfo::ancestry($name)));
array_shift($parents);
foreach ($parents as $parent) {
@ -38,27 +39,27 @@ class SilverStripeServiceConfigurationLocator extends ServiceConfigurationLocato
}
}
}
// there is no parent config, so we'll record that as false so we don't do the expensive
// lookup through parents again
$this->configs[$name] = false;
}
/**
* Retrieves the config for a named service without performing a hierarchy walk
*
*
* @param string $name Name of service
* @return mixed Returns either the configuration data, if there is any. A missing config is denoted
* @return mixed Returns either the configuration data, if there is any. A missing config is denoted
* by a value of either null (there is no direct config assigned and a hierarchy walk is necessary)
* or false (there is no config for this class, nor within the hierarchy for this class).
* or false (there is no config for this class, nor within the hierarchy for this class).
*/
protected function configFor($name) {
// Return cached result
if (isset($this->configs[$name])) {
return $this->configs[$name]; // Potentially false
}
$config = Config::inst()->get('Injector', $name);
if ($config) {
$this->configs[$name] = $config;
@ -67,4 +68,4 @@ class SilverStripeServiceConfigurationLocator extends ServiceConfigurationLocato
return null;
}
}
}
}

View File

@ -3,8 +3,8 @@
/**
* Provides introspection information about the class tree.
*
* It's a cached wrapper around the built-in class functions. SilverStripe uses
* class introspection heavily and without the caching it creates an unfortunate
* It's a cached wrapper around the built-in class functions. SilverStripe uses
* class introspection heavily and without the caching it creates an unfortunate
* performance hit.
*
* @package framework
@ -61,6 +61,7 @@ class ClassInfo {
* @return array List of subclasses
*/
public static function getValidSubClasses($class = 'SiteTree', $includeUnbacked = false) {
$class = self::class_name($class);
$classes = DB::get_schema()->enumValuesForField($class, 'ClassName');
if (!$includeUnbacked) $classes = array_filter($classes, array('ClassInfo', 'exists'));
return $classes;
@ -77,9 +78,7 @@ class ClassInfo {
public static function dataClassesFor($class) {
$result = array();
if (is_object($class)) {
$class = get_class($class);
}
$class = self::class_name($class);
$classes = array_merge(
self::ancestry($class),
@ -101,7 +100,7 @@ class ClassInfo {
* @return string
*/
public static function baseDataClass($class) {
if (is_object($class)) $class = get_class($class);
$class = self::class_name($class);
if (!is_subclass_of($class, 'DataObject')) {
throw new InvalidArgumentException("$class is not a subclass of DataObject");
@ -125,7 +124,7 @@ class ClassInfo {
* <code>
* ClassInfo::subclassesFor('BaseClass');
* array(
* 0 => 'BaseClass',
* 'BaseClass' => 'BaseClass',
* 'ChildClass' => 'ChildClass',
* 'GrandChildClass' => 'GrandChildClass'
* )
@ -135,8 +134,10 @@ class ClassInfo {
* @return array Names of all subclasses as an associative array.
*/
public static function subclassesFor($class) {
//normalise class case
$className = self::class_name($class);
$descendants = SS_ClassLoader::instance()->getManifest()->getDescendantsOf($class);
$result = array($class => $class);
$result = array($className => $className);
if ($descendants) {
return $result + ArrayLib::valuekey($descendants);
@ -145,6 +146,23 @@ class ClassInfo {
}
}
/**
* Convert a class name in any case and return it as it was defined in PHP
*
* eg: self::class_name('dataobJEct'); //returns 'DataObject'
*
* @param string|object $nameOrObject The classname or object you want to normalise
*
* @return string The normalised class name
*/
public static function class_name($nameOrObject) {
if (is_object($nameOrObject)) {
return get_class($nameOrObject);
}
$reflection = new ReflectionClass($nameOrObject);
return $reflection->getName();
}
/**
* Returns the passed class name along with all its parent class names in an
* array, sorted with the root class first.
@ -154,9 +172,11 @@ class ClassInfo {
* @return array
*/
public static function ancestry($class, $tablesOnly = false) {
if (!is_string($class)) $class = get_class($class);
$class = self::class_name($class);
$cacheKey = $class . '_' . (string)$tablesOnly;
$lClass = strtolower($class);
$cacheKey = $lClass . '_' . (string)$tablesOnly;
$parent = $class;
if(!isset(self::$_cache_ancestry[$cacheKey])) {
$ancestry = array();
@ -183,7 +203,7 @@ class ClassInfo {
* Returns true if the given class implements the given interface
*/
public static function classImplements($className, $interfaceName) {
return in_array($className, SS_ClassLoader::instance()->getManifest()->getImplementorsOf($interfaceName));
return in_array($className, self::implementorsOf($interfaceName));
}
/**
@ -232,24 +252,28 @@ class ClassInfo {
private static $method_from_cache = array();
public static function has_method_from($class, $method, $compclass) {
if (!isset(self::$method_from_cache[$class])) self::$method_from_cache[$class] = array();
$lClass = strtolower($class);
$lMethod = strtolower($method);
$lCompclass = strtolower($compclass);
if (!isset(self::$method_from_cache[$lClass])) self::$method_from_cache[$lClass] = array();
if (!array_key_exists($method, self::$method_from_cache[$class])) {
self::$method_from_cache[$class][$method] = false;
if (!array_key_exists($lMethod, self::$method_from_cache[$lClass])) {
self::$method_from_cache[$lClass][$lMethod] = false;
$classRef = new ReflectionClass($class);
if ($classRef->hasMethod($method)) {
$methodRef = $classRef->getMethod($method);
self::$method_from_cache[$class][$method] = $methodRef->getDeclaringClass()->getName();
self::$method_from_cache[$lClass][$lMethod] = $methodRef->getDeclaringClass()->getName();
}
}
return self::$method_from_cache[$class][$method] == $compclass;
return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
}
/**
* Returns the table name in the class hierarchy which contains a given
* Returns the table name in the class hierarchy which contains a given
* field column for a {@link DataObject}. If the field does not exist, this
* will return null.
*
@ -259,23 +283,26 @@ class ClassInfo {
* @return string
*/
public static function table_for_object_field($candidateClass, $fieldName) {
if(!$candidateClass || !$fieldName) {
if(!$candidateClass || !$fieldName || !is_subclass_of($candidateClass, 'DataObject')) {
return null;
}
$exists = class_exists($candidateClass);
//normalise class name
$candidateClass = self::class_name($candidateClass);
$exists = self::exists($candidateClass);
while($candidateClass && $candidateClass != 'DataObject' && $exists) {
if(DataObject::has_own_table($candidateClass)) {
$inst = singleton($candidateClass);
if($inst->hasOwnTableDatabaseField($fieldName)) {
break;
}
}
$candidateClass = get_parent_class($candidateClass);
$exists = class_exists($candidateClass);
$exists = $candidateClass && self::exists($candidateClass);
}
if(!$candidateClass || !$exists) {

View File

@ -39,6 +39,43 @@ records and cannot easily be adapted to include custom `DataObject` instances. T
default site search, have a look at those extensions and modify as required.
</div>
### Fulltext Filter
SilverStripe provides a `[api:FulltextFiler]` which you can use to perform custom fulltext searches on
`[api:DataList]`'s.
Example DataObject:
:::php
class SearchableDataObject extends DataObject {
private static $db = array(
"Title" => "Varchar(255)",
"Content" => "HTMLText",
);
private static $indexes = array(
'SearchFields' => array(
'type' => 'fulltext',
'name' => 'SearchFields',
'value' => '"Title", "Content"',
)
);
private static $create_table_options = array(
'MySQLDatabase' => 'ENGINE=MyISAM'
);
}
Performing the search:
:::php
SearchableDataObject::get()->filter('SearchFields:fulltext', 'search term');
If your search index is a single field size, then you may also specify the search filter by the name of the
field instead of the index.
## API Documentation
* [api:FulltextSearchable]

View File

@ -2,7 +2,7 @@ summary: Describes the process followed for "core" releases.
# Release Process
Describes the process followed for "core" releases (mainly the `framework` and `cms` modules).
This page describes the process followed for "core" releases (mainly the `framework` and `cms` modules).
## Release Maintainer
@ -19,13 +19,13 @@ Release dates are usually not published prior to the release, but you can get a
reviewing the release milestone on github.com. Releases will be
announced on the [release announcements mailing list](http://groups.google.com/group/silverstripe-announce).
Releases of the *cms* and *framework* modules are coupled at the moment, they follow the same numbering scheme.
Releases of the *cms* and *framework* modules are coupled at the moment, and they follow the same numbering scheme.
## Release Numbering
SilverStripe follows [Semantic Versioning](http://semver.org).
Note: Until November 2014, the project didn't adhere to Semantic Versioning. Instead. a "minor release" in semver terminology
Note: Until November 2014, the project didn't adhere to Semantic Versioning. Instead, a "minor release" in semver terminology
was treated as a "major release" in SilverStripe. For example, the *3.1.0* release contained API breaking changes, and
the *3.1.1* release contained new features rather than just bugfixes.
@ -43,7 +43,7 @@ patch release
## Deprecation
Needs of developers (both on core framework and custom projects) can outgrow the capabilities
of a certain API. Existing APIs might turn out to be hard to understand, maintain, test or stabilize.
of a certain API. Existing APIs might turn out to be hard to understand, maintain, test or stabilise.
In these cases, it is best practice to "refactor" these APIs into something more useful.
SilverStripe acknowledges that developers have built a lot of code on top of existing APIs,
so we strive for giving ample warning on any upcoming changes through a "deprecation cycle".
@ -53,17 +53,15 @@ How to deprecate an API:
* Add a `@deprecated` item to the docblock tag, with a `{@link <class>}` item pointing to the new API to use.
* Update the deprecated code to throw a `[api:Deprecation::notice()]` error.
* Both the docblock and error message should contain the **target version** where the functionality is removed.
So if you're committing the change to a *3.1* minor release, the target version will be *4.0*.
So, if you're committing the change to a *3.1* minor release, the target version will be *4.0*.
* Deprecations should not be committed to patch releases
* Deprecations should just be committed to pre-release branches, ideally before they enter the "beta" phase.
* Deprecations should only be committed to pre-release branches, ideally before they enter the "beta" phase.
If deprecations are introduced after this point, their target version needs to be increased by one.
* Make sure that the old deprecated function works by calling the new function - don't have duplicated code!
* The commit message should contain an `API` prefix (see ["commit message format"](code#commit-messages))
* Document the change in the [changelog](/changelogs) for the next release
* Deprecated APIs can be removed after developers had a chance to react to the changes. As a rule of thumb, leave the
code with the deprecation warning in for at least three micro releases. Only remove code in a minor or major release.
* Exceptions to the deprecation cycle are APIs that have been moved into their own module, and continue to work with the
new minor release. These changes can be performed in a single minor release without a deprecation period.
* Deprecated APIs can be removed only after developers have had sufficient time to react to the changes. Hence, deprecated APIs should be removed in MAJOR releases only. Between MAJOR releases, leave the code in place with a deprecation warning.
* Exceptions to the deprecation cycle are APIs that have been moved into their own module, and continue to work with the new minor release. These changes can be performed in a single minor release without a deprecation period.
Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::is_dev()`:
@ -77,8 +75,8 @@ Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::i
return Env::is_dev();
}
This change could be committed to a minor release like *3.2.0*, and stays deprecated in all following minor releases
(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*) where it gets removed from the codebase.
This change could be committed to a minor release like *3.2.0*, and remains deprecated in all subsequent minor releases
(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*), at which point it gets removed from the codebase.
Deprecation notices are enabled by default on dev environment, but can be
turned off via either _ss_environment.php or in your _config.php. Deprecation
@ -118,7 +116,7 @@ previous major release (if applicable).
[new release](http://silverstripe.org/security-releases/) publically.
You can help us determine the problem and speed up responses by providing us with more information on how to reproduce
the issue: SilverStripe version (incl. any installed modules), PHP/webserver version and configuration, anonymized
the issue: SilverStripe version (incl. any installed modules), PHP/webserver version and configuration, anonymised
webserver access logs (if a hack is suspected), any other services and web packages running on the same server.
### Severity rating
@ -128,7 +126,7 @@ each vulnerability. The rating indicates how important an update is:
| Severity | Description |
|---------------|-------------|
| **Critical** | Critical releases require immediate actions. Such vulnerabilities allow attackers to take control of your site and you should upgrade on the day of release. *Example: Directory traversal, privilege escalation* |
| **Critical** | Critical releases require immediate action. Such vulnerabilities allow attackers to take control of your site and you should upgrade on the day of release. *Example: Directory traversal, privilege escalation* |
| **Important** | Important releases should be evaluated immediately. These issues allow an attacker to compromise a site's data and should be fixed within days. *Example: SQL injection.* |
| **Moderate** | Releases of moderate severity should be applied as soon as possible. They allow the unauthorized editing or creation of content. *Examples: Cross Site Scripting (XSS) in template helpers.* |
| **Low** | Low risk releases fix information disclosure and read-only privilege escalation vulnerabilities. These updates should also be applied as soon as possible, but with an impact-dependent priority. *Example: Exposure of the core version number, Cross Site Scripting (XSS) limited to the admin interface.* |
| **Low** | Low risk releases fix information disclosure and read-only privilege escalation vulnerabilities. These updates should also be applied as soon as possible, but according to an impact-dependent priority. *Example: Exposure of the core version number, Cross Site Scripting (XSS) limited to the admin interface.* |

View File

@ -841,7 +841,7 @@ class File extends DataObject {
'htm' => _t('File.HtmlType', 'HTML file')
);
$ext = $this->getExtension();
$ext = strtolower($this->getExtension());
return isset($types[$ext]) ? $types[$ext] : 'unknown';
}

View File

@ -67,37 +67,44 @@ if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
*/
global $url;
// PHP 5.4's built-in webserver uses this
if (php_sapi_name() == 'cli-server') {
$url = $_SERVER['REQUEST_URI'];
// Helper to safely parse and load a querystring fragment
$parseQuery = function($query) {
parse_str($query, $_GET);
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
};
// Querystring args need to be explicitly parsed
if(strpos($url,'?') !== false) {
list($url, $query) = explode('?',$url,2);
parse_str($query, $_GET);
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
// Apache rewrite rules and IIS use this
if (isset($_GET['url']) && php_sapi_name() !== 'cli-server') {
// Prevent injection of url= querystring argument by prioritising any leading url argument
if(isset($_SERVER['QUERY_STRING']) &&
preg_match('/^(?<url>url=[^&?]*)(?<query>.*[&?]url=.*)$/', $_SERVER['QUERY_STRING'], $results)
) {
$queryString = $results['query'].'&'.$results['url'];
$parseQuery($queryString);
}
// Pass back to the webserver for files that exist
if(file_exists(BASE_PATH . $url) && is_file(BASE_PATH . $url)) return false;
// Apache rewrite rules use this
} else if (isset($_GET['url'])) {
$url = $_GET['url'];
// IIS includes get variables in url
$i = strpos($url, '?');
if($i !== false) {
$url = substr($url, 0, $i);
}
// Lighttpd uses this
// Lighttpd and PHP 5.4's built-in webserver use this
} else {
if(strpos($_SERVER['REQUEST_URI'],'?') !== false) {
list($url, $query) = explode('?', $_SERVER['REQUEST_URI'], 2);
parse_str($query, $_GET);
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
} else {
$url = $_SERVER["REQUEST_URI"];
$url = $_SERVER['REQUEST_URI'];
// Querystring args need to be explicitly parsed
if(strpos($url,'?') !== false) {
list($url, $query) = explode('?',$url,2);
$parseQuery($query);
}
// Pass back to the webserver for files that exist
if(php_sapi_name() === 'cli-server' && file_exists(BASE_PATH . $url) && is_file(BASE_PATH . $url)) {
return false;
}
}

View File

@ -369,7 +369,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!isset(DataObject::$_cache_composite_fields[$class])) {
self::cache_composite_fields($class);
}
if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
$isComposite = DataObject::$_cache_composite_fields[$class][$name];
} elseif($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
@ -452,6 +452,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$record = null;
}
if(is_a($record, "stdClass")) {
$record = (array)$record;
}
// Set $this->record to $record, but ignore NULLs
$this->record = array();
foreach($record as $k => $v) {
@ -1263,6 +1267,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
continue;
}
// if database column doesn't correlate to a DBField instance...
$fieldObj = $this->dbObject($fieldName);
if(!$fieldObj) {
@ -1679,7 +1684,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} else {
$remoteClass = $this->belongsToComponent($component, false);
}
if(empty($remoteClass)) {
throw new Exception("Unknown $type component '$component' on class '$this->class'");
}
@ -2742,6 +2747,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public static function has_own_table($dataClass) {
if(!is_subclass_of($dataClass,'DataObject')) return false;
$dataClass = ClassInfo::class_name($dataClass);
if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
if(get_parent_class($dataClass) == 'DataObject') {
DataObject::$cache_has_own_table[$dataClass] = true;
@ -3128,6 +3134,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $result;
}
/**
* Return the first item matching the given query.
* All calls to get_one() are cached.

View File

@ -196,7 +196,7 @@ class DataQuery {
$tableClasses = $ancestorTables;
}
$tableNames = array_keys($tableClasses);
$tableNames = array_values($tableClasses);
$baseClass = $tableNames[0];
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
@ -828,7 +828,9 @@ class DataQuery {
/**
* Represents a subgroup inside a WHERE clause in a {@link DataQuery}
*
* Stores the clauses for the subgroup inside a specific {@link SQLQuery} object.
* Stores the clauses for the subgroup inside a specific {@link SQLQuery}
* object.
*
* All non-where methods call their DataQuery versions, which uses the base
* query object.
*

View File

@ -84,7 +84,9 @@ abstract class StringField extends DBField {
* @see core/model/fieldtypes/DBField#exists()
*/
public function exists() {
return ($this->value || $this->value == '0') || ( !$this->nullifyEmpty && $this->value === '');
return $this->getValue() // All truthy values exist
|| (is_string($this->getValue()) && strlen($this->getValue())) // non-empty strings exist ('0' but not (int)0)
|| (!$this->getNullifyEmpty() && $this->getValue() === ''); // Remove this stupid exemption in 4.0
}
/**

40
search/filters/FulltextFilter.php Normal file → Executable file
View File

@ -17,22 +17,23 @@
* database table, using the {$indexes} hash in your DataObject subclass:
*
* <code>
* static $indexes = array(
* private static $indexes = array(
* 'SearchFields' => 'fulltext(Name, Title, Description)'
* );
* </code>
*
* @package framework
* @subpackage search
* @todo Add support for databases besides MySQL
*/
class FulltextFilter extends SearchFilter {
protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$predicate = sprintf("MATCH (%s) AGAINST (?)", $this->getDbName());
return $query->where(array($predicate => $this->getValue()));
}
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$predicate = sprintf("NOT MATCH (%s) AGAINST (?)", $this->getDbName());
return $query->where(array($predicate => $this->getValue()));
}
@ -40,4 +41,37 @@ class FulltextFilter extends SearchFilter {
public function isEmpty() {
return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
}
/**
* This implementation allows for a list of columns to be passed into MATCH() instead of just one.
*
* @example
* <code>
* MyDataObject::get()->filter('SearchFields:fulltext', 'search term')
* </code>
*
* @return string
*/
public function getDbName() {
$indexes = Config::inst()->get($this->model, "indexes");
if(is_array($indexes) && array_key_exists($this->getName(), $indexes)) {
$index = $indexes[$this->getName()];
if(is_array($index) && array_key_exists("value", $index)) {
return $index['value'];
} else {
// Parse a fulltext string (eg. fulltext ("ColumnA", "ColumnB")) to figure out which columns
// we need to search.
if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) {
return $matches[1];
} else {
throw new Exception("Invalid fulltext index format for '" . $this->getName()
. "' on '" . $this->model . "'");
}
}
}
return parent::getDbName();
}
}

View File

@ -164,6 +164,12 @@ abstract class SearchFilter extends Object {
if($this->name == "NULL") {
return $this->name;
}
// Ensure that we're dealing with a DataObject.
if (!is_subclass_of($this->model, 'DataObject')) {
throw new InvalidArgumentException(
"Model supplied to " . get_class($this) . " should be an instance of DataObject."
);
}
$candidateClass = ClassInfo::table_for_object_field(
$this->model,
@ -177,7 +183,7 @@ abstract class SearchFilter extends Object {
return '"' . implode('"."', $parts) . '"';
}
return "\"$candidateClass\".\"$this->name\"";
return sprintf('"%s"."%s"', $candidateClass, $this->name);
}
/**
@ -194,7 +200,6 @@ abstract class SearchFilter extends Object {
return $dbField->RAW();
}
/**
* Apply filter criteria to a SQL query.
*

View File

@ -14,10 +14,18 @@ class ClassInfoTest extends SapphireTest {
'ClassInfoTest_NoFields',
);
public function setUp() {
parent::setUp();
ClassInfo::reset_db_cache();
}
public function testExists() {
$this->assertTrue(ClassInfo::exists('Object'));
$this->assertTrue(ClassInfo::exists('object'));
$this->assertTrue(ClassInfo::exists('ClassInfoTest'));
$this->assertTrue(ClassInfo::exists('CLASSINFOTEST'));
$this->assertTrue(ClassInfo::exists('stdClass'));
$this->assertTrue(ClassInfo::exists('stdCLASS'));
}
public function testSubclassesFor() {
@ -30,6 +38,16 @@ class ClassInfoTest extends SapphireTest {
),
'ClassInfo::subclassesFor() returns only direct subclasses and doesnt include base class'
);
ClassInfo::reset_db_cache();
$this->assertEquals(
ClassInfo::subclassesFor('classinfotest_baseclass'),
array(
'ClassInfoTest_BaseClass' => 'ClassInfoTest_BaseClass',
'ClassInfoTest_ChildClass' => 'ClassInfoTest_ChildClass',
'ClassInfoTest_GrandChildClass' => 'ClassInfoTest_GrandChildClass'
),
'ClassInfo::subclassesFor() is acting in a case sensitive way when it should not'
);
}
public function testClassesForFolder() {
@ -42,11 +60,11 @@ class ClassInfoTest extends SapphireTest {
$classes,
'ClassInfo::classes_for_folder() returns classes matching the filename'
);
// $this->assertContains(
// 'ClassInfoTest_BaseClass',
// $classes,
// 'ClassInfo::classes_for_folder() returns additional classes not matching the filename'
// );
$this->assertContains(
'classinfotest_baseclass',
$classes,
'ClassInfo::classes_for_folder() returns additional classes not matching the filename'
);
}
/**
@ -54,8 +72,11 @@ class ClassInfoTest extends SapphireTest {
*/
public function testBaseDataClass() {
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_BaseClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('classinfotest_baseclass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_ChildClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('CLASSINFOTEST_CHILDCLASS'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GrandChildClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GRANDChildClass'));
$this->setExpectedException('InvalidArgumentException');
ClassInfo::baseDataClass('DataObject');
@ -75,6 +96,13 @@ class ClassInfoTest extends SapphireTest {
));
$this->assertEquals($expect, $ancestry);
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::ancestry('classINFOTest_Childclass'));
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::ancestry('classINFOTest_Childclass'));
ClassInfo::reset_db_cache();
$ancestry = ClassInfo::ancestry('ClassInfoTest_ChildClass', true);
$this->assertEquals(array('ClassInfoTest_BaseClass' => 'ClassInfoTest_BaseClass'), $ancestry,
'$tablesOnly option excludes memory-only inheritance classes'
@ -97,16 +125,22 @@ class ClassInfoTest extends SapphireTest {
'ClassInfoTest_HasFields',
);
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::dataClassesFor($classes[0]));
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::dataClassesFor(strtoupper($classes[0])));
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::dataClassesFor($classes[1]));
$expect = array(
'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass',
'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields',
);
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::dataClassesFor($classes[2]));
ClassInfo::reset_db_cache();
$this->assertEquals($expect, ClassInfo::dataClassesFor(strtolower($classes[2])));
}
public function testTableForObjectField() {
@ -114,19 +148,27 @@ class ClassInfoTest extends SapphireTest {
ClassInfo::table_for_object_field('ClassInfoTest_WithRelation', 'RelationID')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
$this->assertEquals('ClassInfoTest_WithRelation',
ClassInfo::table_for_object_field('ClassInfoTest_withrelation', 'RelationID')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_BaseDataClass', 'Title')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Title')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_NoFields', 'Title')
);
$this->assertEquals('ClassInfoTest_HasFields',
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('classinfotest_nofields', 'Title')
);
$this->assertEquals('ClassInfoTest_HasFields',
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Description')
);

View File

@ -250,6 +250,9 @@ class FileTest extends SapphireTest {
$file = $this->objFromFixture('File', 'pdf');
$this->assertEquals("Adobe Acrobat PDF file", $file->FileType);
$file = $this->objFromFixture('File', 'gifupper');
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
/* Only a few file types are given special descriptions; the rest are unknown */
$file = $this->objFromFixture('File', 'asdf');
$this->assertEquals("unknown", $file->FileType);

View File

@ -13,6 +13,8 @@ File:
Filename: assets/FileTest.txt
gif:
Filename: assets/FileTest.gif
gifupper:
Filename: assets/FileTest.GIF
pdf:
Filename: assets/FileTest.pdf
setfromname:

View File

@ -146,6 +146,11 @@ class DataListTest extends SapphireTest {
$this->assertEquals('DataObjectTest_TeamComment',$list->dataClass());
}
public function testDataClassCaseInsensitive() {
$list = DataList::create('dataobjecttest_teamcomment');
$this->assertTrue($list->exists());
}
public function testClone() {
$list = DataObjectTest_TeamComment::get();
$this->assertEquals($list, clone($list));

View File

@ -61,6 +61,30 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals('Comment', key($dbFields), 'DataObject::db returns fields in correct order');
}
public function testConstructAcceptsValues() {
// Values can be an array...
$player = new DataObjectTest_Player(array(
'FirstName' => 'James',
'Surname' => 'Smith'
));
$this->assertEquals('James', $player->FirstName);
$this->assertEquals('Smith', $player->Surname);
// ... or a stdClass inst
$data = new stdClass();
$data->FirstName = 'John';
$data->Surname = 'Doe';
$player = new DataObjectTest_Player($data);
$this->assertEquals('John', $player->FirstName);
$this->assertEquals('Doe', $player->Surname);
// IDs should be stored as integers, not strings
$player = new DataObjectTest_Player(array('ID' => '5'));
$this->assertSame(5, $player->ID);
}
public function testValidObjectsForBaseFields() {
$obj = new DataObjectTest_ValidatedObject();

View File

@ -36,6 +36,22 @@ class StringFieldTest extends SapphireTest {
);
}
public function testExists() {
// True exists
$this->assertTrue(DBField::create_field('StringFieldTest_MyStringField', true)->exists());
$this->assertTrue(DBField::create_field('StringFieldTest_MyStringField', '0')->exists());
$this->assertTrue(DBField::create_field('StringFieldTest_MyStringField', '1')->exists());
$this->assertTrue(DBField::create_field('StringFieldTest_MyStringField', 1)->exists());
$this->assertTrue(DBField::create_field('StringFieldTest_MyStringField', 1.1)->exists());
// false exists
$this->assertFalse(DBField::create_field('StringFieldTest_MyStringField', false)->exists());
$this->assertFalse(DBField::create_field('StringFieldTest_MyStringField', '')->exists());
$this->assertFalse(DBField::create_field('StringFieldTest_MyStringField', null)->exists());
$this->assertFalse(DBField::create_field('StringFieldTest_MyStringField', 0)->exists());
$this->assertFalse(DBField::create_field('StringFieldTest_MyStringField', 0.0)->exists());
}
}
class StringFieldTest_MyStringField extends StringField implements TestOnly {

View File

@ -0,0 +1,105 @@
<?php
class FulltextFilterTest extends SapphireTest {
protected $extraDataObjects = array(
'FulltextFilterTest_DataObject'
);
protected static $fixture_file = "FulltextFilterTest.yml";
public function testFilter() {
if(DB::getConn() instanceof MySQLDatabase) {
$baseQuery = FulltextFilterTest_DataObject::get();
$this->assertEquals(3, $baseQuery->count(), "FulltextFilterTest_DataObject count does not match.");
// First we'll text the 'SearchFields' which has been set using an array
$search = $baseQuery->filter("SearchFields:fulltext", 'SilverStripe');
$this->assertEquals(1, $search->count());
$search = $baseQuery->exclude("SearchFields:fulltext", "SilverStripe");
$this->assertEquals(2, $search->count());
// Now we'll run the same tests on 'OtherSearchFields' which should yield the same resutls
// but has been set using a string.
$search = $baseQuery->filter("OtherSearchFields:fulltext", 'SilverStripe');
$this->assertEquals(1, $search->count());
$search = $baseQuery->exclude("OtherSearchFields:fulltext", "SilverStripe");
$this->assertEquals(2, $search->count());
// Search on a single field
$search = $baseQuery->filter("ColumnE:fulltext", 'Dragons');
$this->assertEquals(1, $search->count());
$search = $baseQuery->exclude("ColumnE:fulltext", "Dragons");
$this->assertEquals(2, $search->count());
} else {
$this->markTestSkipped("FulltextFilter only supports MySQL syntax.");
}
}
public function testGenerateQuery() {
// Test SearchFields
$filter1 = new FulltextFilter('SearchFields', 'SilverStripe');
$filter1->setModel('FulltextFilterTest_DataObject');
$query1 = FulltextFilterTest_DataObject::get()->dataQuery();
$filter1->apply($query1);
$this->assertEquals('"ColumnA", "ColumnB"', $filter1->getDbName());
$this->assertEquals(
array("MATCH (\"ColumnA\", \"ColumnB\") AGAINST ('SilverStripe')"),
$query1->query()->getWhere()
);
// Test Other searchfields
$filter2 = new FulltextFilter('OtherSearchFields', 'SilverStripe');
$filter2->setModel('FulltextFilterTest_DataObject');
$query2 = FulltextFilterTest_DataObject::get()->dataQuery();
$filter2->apply($query2);
$this->assertEquals('"ColumnC", "ColumnD"', $filter2->getDbName());
$this->assertEquals(
array("MATCH (\"ColumnC\", \"ColumnD\") AGAINST ('SilverStripe')"),
$query2->query()->getWhere()
);
// Test fallback to single field
$filter3 = new FulltextFilter('ColumnA', 'SilverStripe');
$filter3->setModel('FulltextFilterTest_DataObject');
$query3 = FulltextFilterTest_DataObject::get()->dataQuery();
$filter3->apply($query3);
$this->assertEquals('"FulltextFilterTest_DataObject"."ColumnA"', $filter3->getDbName());
$this->assertEquals(
array("MATCH (\"FulltextFilterTest_DataObject\".\"ColumnA\") AGAINST ('SilverStripe')"),
$query3->query()->getWhere()
);
}
}
class FulltextFilterTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
"ColumnA" => "Varchar(255)",
"ColumnB" => "HTMLText",
"ColumnC" => "Varchar(255)",
"ColumnD" => "HTMLText",
"ColumnE" => 'Varchar(255)'
);
private static $indexes = array(
'SearchFields' => array(
'type' => 'fulltext',
'name' => 'SearchFields',
'value' => '"ColumnA", "ColumnB"',
),
'OtherSearchFields' => 'fulltext ("ColumnC", "ColumnD")',
'SingleIndex' => 'fulltext ("ColumnE")'
);
private static $create_table_options = array(
"MySQLDatabase" => "ENGINE=MyISAM",
);
}

View File

@ -0,0 +1,19 @@
FulltextFilterTest_DataObject:
object1:
ColumnA: 'SilverStripe'
CluumnB: '<p>Some content about SilverStripe.</p>'
ColumnC: 'SilverStripe'
ColumnD: '<p>Some content about SilverStripe.</p>'
ColumnE: 'Dragons be here'
object2:
ColumnA: 'Test Row'
ColumnB: '<p>Some information about this test row.</p>'
ColumnC: 'Test Row'
ColumnD: '<p>Some information about this test row.</p>'
ColumnE: 'No'
object3:
ColumnA: 'Fulltext Search'
ColumnB: '<p>Testing fulltext search.</p>'
ColumnC: 'Fulltext Search'
ColumnD: '<p>Testing fulltext search.</p>'
ColumnE: ''

View File

@ -1 +1 @@
$Title <% if ArgA %>- $ArgA <% end_if %>- <%if First %>First-<% end_if %><% if Last %>Last-<% end_if %><%if MultipleOf(2) %>EVEN<% else %>ODD<% end_if %> top:$Top.Title
<% if $Title %>$Title<% else %>Untitled<% end_if %> <% if $ArgA %>_ $ArgA <% end_if %>- <% if $First %>First-<% end_if %><% if $Last %>Last-<% end_if %><%if $MultipleOf(2) %>EVEN<% else %>ODD<% end_if %> top:$Top.Title

View File

@ -52,17 +52,45 @@ class SSViewerTest extends SapphireTest {
// reset results for the tests that include arguments (the title is passed as an arg)
$expected = array(
'Item 1 - Item 1 - First-ODD top:Item 1',
'Item 2 - Item 2 - EVEN top:Item 2',
'Item 3 - Item 3 - ODD top:Item 3',
'Item 4 - Item 4 - EVEN top:Item 4',
'Item 5 - Item 5 - ODD top:Item 5',
'Item 6 - Item 6 - Last-EVEN top:Item 6',
'Item 1 _ Item 1 - First-ODD top:Item 1',
'Item 2 _ Item 2 - EVEN top:Item 2',
'Item 3 _ Item 3 - ODD top:Item 3',
'Item 4 _ Item 4 - EVEN top:Item 4',
'Item 5 _ Item 5 - ODD top:Item 5',
'Item 6 _ Item 6 - Last-EVEN top:Item 6',
);
$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
$this->assertExpectedStrings($result, $expected);
}
public function testIncludeTruthyness() {
$data = new ArrayData(array(
'Title' => 'TruthyTest',
'Items' => new ArrayList(array(
new ArrayData(array('Title' => 'Item 1')),
new ArrayData(array('Title' => '')),
new ArrayData(array('Title' => true)),
new ArrayData(array('Title' => false)),
new ArrayData(array('Title' => null)),
new ArrayData(array('Title' => 0)),
new ArrayData(array('Title' => 7))
))
));
$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
// We should not end up with empty values appearing as empty
$expected = array(
'Item 1 _ Item 1 - First-ODD top:Item 1',
'Untitled - EVEN top:',
'1 _ 1 - ODD top:1',
'Untitled - EVEN top:',
'Untitled - ODD top:',
'Untitled - EVEN top:0',
'7 _ 7 - Last-ODD top:7'
);
$this->assertExpectedStrings($result, $expected);
}
private function getScopeInheritanceTestData() {
return new ArrayData(array(

View File

@ -441,6 +441,15 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
}
}
/**
* Get the injected value
*
* @param string $property Name of property
* @param array $params
* @param bool $cast If true, an object is always returned even if not an object.
* @return array Result array with the keys 'value' for raw value, or 'obj' if contained in an object
* @throws InvalidArgumentException
*/
public function getInjectedValue($property, $params, $cast = true) {
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
@ -524,32 +533,25 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
if (isset($arguments[1]) && $arguments[1] != null) $params = $arguments[1];
else $params = array();
$hasInjected = $res = null;
if ($name == 'hasValue') {
if ($val = $this->getInjectedValue($property, $params, false)) {
$hasInjected = true; $res = (bool)$val['value'];
}
}
else { // XML_val
if ($val = $this->getInjectedValue($property, $params)) {
$hasInjected = true;
$obj = $val['obj'];
$val = $this->getInjectedValue($property, $params);
if ($val) {
$obj = $val['obj'];
if ($name === 'hasValue') {
$res = $obj instanceof Object
? $obj->exists()
: (bool)$obj;
} else {
// XML_val
$res = $obj->forTemplate();
}
}
if ($hasInjected) {
$this->resetLocalScope();
return $res;
}
else {
} else {
return parent::__call($name, $arguments);
}
}
}
/**
* Parses a template file with an *.ss file extension.
*