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

Conflicts:
	css/AssetUploadField.css
This commit is contained in:
Damian Mooyman 2015-07-31 14:33:16 +12:00
commit e0a560051e
60 changed files with 806 additions and 251 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
@ -1622,7 +1622,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

@ -272,8 +272,9 @@
complete: function(xmlhttp, status) {
button.removeClass('loading');
// Deselect all nodes
tree.jstree('uncheck_all');
// Refresh the tree.
// Makes sure all nodes have the correct CSS classes applied.
tree.jstree('refresh', -1);
self.setIDs([]);
// Reset action

View File

@ -36,7 +36,7 @@ $border: 1px solid darken(#D9D9D9, 15%);
-webkit-box-shadow: none;
}
li{
@include background-image(linear-gradient(top, #f8f8f8, #D9D9D9));
@include background-image(linear-gradient(top, #f8f8f8, #D9D9D9));
@include border-radius(0);
background: #eaeaea;
border: none;
@ -125,7 +125,7 @@ $border: 1px solid darken(#D9D9D9, 15%);
font-size: 12px;
}
#Form_AddForm_PageType_Holder ul {
#Form_AddForm_PageType ul {
padding: 0;
li{

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

@ -308,7 +308,7 @@ class HTTP {
/**
* Add the appropriate caching headers to the response, including If-Modified-Since / 304 handling.
*
* @param SS_HTTPResponse The SS_HTTPResponse object to augment. Omitted the argument or passing a string is
* @param SS_HTTPResponse $body The SS_HTTPResponse object to augment. Omitted the argument or passing a string is
* deprecated; in these cases, the headers are output directly.
*/
public static function add_cache_headers($body = null) {
@ -328,21 +328,17 @@ class HTTP {
// us trying.
if(headers_sent() && !$body) return;
// Popuplate $responseHeaders with all the headers that we want to build
// Populate $responseHeaders with all the headers that we want to build
$responseHeaders = array();
$config = Config::inst();
$cacheControlHeaders = Config::inst()->get('HTTP', 'cache_control');
// currently using a config setting to cancel this, seems to be so taht the CMS caches ajax requests
// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests
if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) {
$requestHeaders = apache_request_headers();
$requestHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER);
if(isset($requestHeaders['X-Requested-With']) && $requestHeaders['X-Requested-With']=='XMLHttpRequest') {
$cacheAge = 0;
}
// bdc: now we must check for DUMB IE6:
if(isset($requestHeaders['x-requested-with']) && $requestHeaders['x-requested-with']=='XMLHttpRequest') {
$cacheAge = 0;
}
@ -383,13 +379,16 @@ class HTTP {
foreach($cacheControlHeaders as $header => $value) {
if(is_null($value)) {
unset($cacheControlHeaders[$header]);
} elseif(is_bool($value) || $value === "true") {
} elseif((is_bool($value) && $value) || $value === "true") {
$cacheControlHeaders[$header] = $header;
} else {
$cacheControlHeaders[$header] = $header."=".$value;
}
}
$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
unset($cacheControlHeaders, $header, $value);
if(self::$modification_date && $cacheAge > 0) {
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);

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

@ -122,6 +122,7 @@ if(!defined('TRUSTED_PROXY')) {
*/
if(!isset($_SERVER['HTTP_HOST'])) {
// HTTP_HOST, REQUEST_PORT, SCRIPT_NAME, and PHP_SELF
global $_FILE_TO_URL_MAPPING;
if(isset($_FILE_TO_URL_MAPPING)) {
$fullPath = $testPath = realpath($_SERVER['SCRIPT_FILENAME']);
while($testPath && $testPath != '/' && !preg_match('/^[A-Z]:\\\\$/', $testPath)) {

View File

@ -41,7 +41,7 @@ body.cms.ss-uploadfield-edit-iframe .fieldholder-small label, .composite.ss-asse
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info { background-color: #c11f1d; padding-right: 130px; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2MxMWYxZCIvPjxzdG9wIG9mZnNldD0iNCUiIHN0b3AtY29sb3I9IiNiZjFkMWIiLz48c3RvcCBvZmZzZXQ9IjglIiBzdG9wLWNvbG9yPSIjYjcxYjFjIi8+PHN0b3Agb2Zmc2V0PSIxNSUiIHN0b3AtY29sb3I9IiNiNjFlMWQiLz48c3RvcCBvZmZzZXQ9IjI3JSIgc3RvcC1jb2xvcj0iI2IxMWQxZCIvPjxzdG9wIG9mZnNldD0iMzElIiBzdG9wLWNvbG9yPSIjYWIxZDFjIi8+PHN0b3Agb2Zmc2V0PSI0MiUiIHN0b3AtY29sb3I9IiNhNTFiMWIiLz48c3RvcCBvZmZzZXQ9IjQ2JSIgc3RvcC1jb2xvcj0iIzlmMWIxOSIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjOWYxYjE5Ii8+PHN0b3Agb2Zmc2V0PSI1NCUiIHN0b3AtY29sb3I9IiM5OTFjMWEiLz48c3RvcCBvZmZzZXQ9IjU4JSIgc3RvcC1jb2xvcj0iIzk3MWExOCIvPjxzdG9wIG9mZnNldD0iNjIlIiBzdG9wLWNvbG9yPSIjOTExYjFiIi8+PHN0b3Agb2Zmc2V0PSI2NSUiIHN0b3AtY29sb3I9IiM5MTFiMWIiLz48c3RvcCBvZmZzZXQ9Ijg4JSIgc3RvcC1jb2xvcj0iIzdlMTgxNiIvPjxzdG9wIG9mZnNldD0iOTIlIiBzdG9wLWNvbG9yPSIjNzcxOTE5Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNzMxODE3Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g'); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #c11f1d), color-stop(4%, #bf1d1b), color-stop(8%, #b71b1c), color-stop(15%, #b61e1d), color-stop(27%, #b11d1d), color-stop(31%, #ab1d1c), color-stop(42%, #a51b1b), color-stop(46%, #9f1b19), color-stop(50%, #9f1b19), color-stop(54%, #991c1a), color-stop(58%, #971a18), color-stop(62%, #911b1b), color-stop(65%, #911b1b), color-stop(88%, #7e1816), color-stop(92%, #771919), color-stop(100%, #731817)); background-image: -moz-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: -webkit-linear-gradient(top, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); background-image: linear-gradient(to bottom, #c11f1d 0%, #bf1d1b 4%, #b71b1c 8%, #b61e1d 15%, #b11d1d 27%, #ab1d1c 31%, #a51b1b 42%, #9f1b19 46%, #9f1b19 50%, #991c1a 54%, #971a18 58%, #911b1b 62%, #911b1b 65%, #7e1816 88%, #771919 92%, #731817 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name { width: 100%; cursor: default; background: #bcb9b9; background: rgba(201, 198, 198, 0.9); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-error .ss-uploadfield-item-info .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.7); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #E9D104; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U1ZDMzYiIvPjxzdG9wIG9mZnNldD0iOCUiIHN0b3AtY29sb3I9IiNlMmNlMjQiLz48c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iI2QxYmUxYyIvPjxzdG9wIG9mZnNldD0iNTQlIiBzdG9wLWNvbG9yPSIjZDFiYzFiIi8+PHN0b3Agb2Zmc2V0PSI5NiUiIHN0b3AtY29sb3I9IiNkMDlhMWEiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjZTg3MTkiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyYWQpIiAvPjwvc3ZnPiA='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e5d33b), color-stop(8%, #e2ce24), color-stop(50%, #d1be1c), color-stop(54%, #d1bc1b), color-stop(96%, #d09a1a), color-stop(100%, #ce8719)); background-image: -moz-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: -webkit-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); background-image: linear-gradient(to bottom, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bc1b 54%, #d09a1a 96%, #ce8719 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ui-state-warning .ss-uploadfield-item-info { background-color: #E9D104; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2U1ZDMzYiIvPjxzdG9wIG9mZnNldD0iOCUiIHN0b3AtY29sb3I9IiNlMmNlMjQiLz48c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iI2QxYmUxYyIvPjxzdG9wIG9mZnNldD0iNTQlIiBzdG9wLWNvbG9yPSIjZDFiZDFjIi8+PHN0b3Agb2Zmc2V0PSI5NiUiIHN0b3AtY29sb3I9IiNkMDlhMWEiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjZjg3MWEiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyYWQpIiAvPjwvc3ZnPiA='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e5d33b), color-stop(8%, #e2ce24), color-stop(50%, #d1be1c), color-stop(54%, #d1bd1c), color-stop(96%, #d09a1a), color-stop(100%, #cf871a)); background-image: -moz-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bd1c 54%, #d09a1a 96%, #cf871a 100%); background-image: -webkit-linear-gradient(top, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bd1c 54%, #d09a1a 96%, #cf871a 100%); background-image: linear-gradient(to bottom, #e5d33b 0%, #e2ce24 8%, #d1be1c 50%, #d1bd1c 54%, #d09a1a 96%, #cf871a 100%); }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name { position: relative; z-index: 1; margin: 3px 0 3px 50px; width: 50%; color: #7f8c97; background: #eeeded; background: rgba(255, 255, 255, 0.8); -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; line-height: 24px; height: 22px; padding: 0 5px; text-align: left; cursor: pointer; display: table; table-layout: fixed; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .name { text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); display: inline; float: left; max-width: 50%; font-weight: normal; padding: 0 5px 0 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; }
.ss-assetuploadfield .ss-uploadfield-files .ss-uploadfield-item-name .ss-uploadfield-item-status { position: relative; float: right; padding: 0 0 0 5px; max-width: 30%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); }

View File

@ -42,6 +42,19 @@ class Deprecation {
*/
protected static $version;
/**
* Override whether deprecation is enabled. If null, then fallback to
* SS_DEPRECATION_ENABLED, and then true if not defined.
*
* Deprecation is only available on dev.
*
* Must be configured outside of the config API, as deprecation API
* must be available before this to avoid infinite loops.
*
* @var boolean|null
*/
protected static $enabled = null;
/**
*
* @var array
@ -116,20 +129,47 @@ class Deprecation {
return $called['function'];
}
}
/**
* Determine if deprecation notices should be displayed
*
* @return bool
*/
public static function get_enabled() {
// Deprecation is only available on dev
if(!Director::isDev()) {
return false;
}
if(isset(self::$enabled)) {
return self::$enabled;
}
if(defined('SS_DEPRECATION_ENABLED')) {
return SS_DEPRECATION_ENABLED;
}
return true;
}
/**
* Toggle on or off deprecation notices. Will be ignored in live.
*
* @param bool $enabled
*/
public static function set_enabled($enabled) {
self::$enabled = $enabled;
}
/**
* Raise a notice indicating the method is deprecated if the version passed as the second argument is greater
* than or equal to the check version set via ::notification_version
*
* @static
* @param $string - The notice to raise
* @param $atVersion - The version at which this notice should start being raised
* @param Boolean $scope - Notice relates to the method or class context its called in.
* @return void
* @param string $atVersion The version at which this notice should start being raised
* @param string $string The notice to raise
* @param bool $scope Notice relates to the method or class context its called in.
*/
public static function notice($atVersion, $string = '', $scope = Deprecation::SCOPE_METHOD) {
// Never raise deprecation notices in a live environment
if(Director::isLive(true)) return;
if(!static::get_enabled()) {
return;
}
$checkVersion = self::$version;
// Getting a backtrace is slow, so we only do it if we need it
@ -179,25 +219,27 @@ class Deprecation {
/**
* Method for when testing. Dump all the current version settings to a variable for later passing to restore
* @return array - opaque array that should only be used to pass to ::restore_version_settings
*
* @return array Opaque array that should only be used to pass to {@see Deprecation::restore_settings()}
*/
public static function dump_settings() {
return array(
'level' => self::$notice_level,
'version' => self::$version,
'moduleVersions' => self::$module_version_overrides
'moduleVersions' => self::$module_version_overrides,
'enabled' => self::$enabled,
);
}
/**
* Method for when testing. Restore all the current version settings from a variable
* @static
* @param $settings array - An array as returned by ::dump_version_settings
* @return void
*
* @param $settings array An array as returned by {@see Deprecation::dump_settings()}
*/
public static function restore_settings($settings) {
self::$notice_level = $settings['level'];
self::$version = $settings['version'];
self::$module_version_overrides = $settings['moduleVersions'];
self::$enabled = $settings['enabled'];
}
}

View File

@ -116,6 +116,7 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th
| `SS_DATABASE_TIMEZONE`| Set the database timezone to something other than the system timezone.
| `SS_DATABASE_NAME` | Set the database name. Assumes the `$database` global variable in your config is missing or empty. |
| `SS_DATABASE_CHOOSE_NAME`| Boolean/Int. If defined, then the system will choose a default database name for you if one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of the box without the installer needing to alter any files. This helps prevent accidental changes to the environment. If `SS_DATABASE_CHOOSE_NAME` is an integer greater than one, then an ancestor folder will be used for the database name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.|
| `SS_DEPRECATION_ENABLED` | Enable deprecation notices for this environment.|
| `SS_ENVIRONMENT_TYPE`| The environment type: dev, test or live.|
| `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.|
| `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.|

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

@ -73,6 +73,8 @@
* Security: The multiple authenticator login page should now be styled manually - i.e. without the default jQuery
UI layout. A new template, Security_MultiAuthenticatorLogin.ss is available.
* Security: This controller's templates can be customised by overriding the `getTemplatesFor` function.
* `Deprecation::set_enabled()` or `SS_DEPRECATION_ENABLED` can now be used to
enable or disable deprecation notices. Deprecation notices are no longer displayed on test.
* API: Form and FormField ID attributes rewritten.
* `SearchForm::getSearchQuery` no longer pre-escapes search keywords and must
be cast in your template

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,27 @@ 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
notices are always disabled on both live and test.
`mysite/_config.php`
:::php
Deprecation::set_enabled(false);
`_ss_environment.php`
:::php
define('SS_DEPRECATION_ENABLED', false);
## Security Releases
@ -99,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
@ -109,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

@ -221,6 +221,15 @@ class File extends DataObject {
}
}
/**
* A file only exists if the file_exists() and is in the DB as a record
*
* @return bool
*/
public function exists() {
return parent::exists() && file_exists($this->getFullPath());
}
/**
* Find a File object by the given filename.
*
@ -293,7 +302,7 @@ class File extends DataObject {
// ensure that the record is synced with the filesystem before deleting
$this->updateFilesystem();
if($this->Filename && $this->Name && file_exists($this->getFullPath()) && !is_dir($this->getFullPath())) {
if($this->exists() && !is_dir($this->getFullPath())) {
unlink($this->getFullPath());
}
}
@ -832,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

@ -120,7 +120,7 @@ class ConfirmedPasswordField extends FormField {
/**
* @param array $properties
*
* @return string
* @return HTMLText
*/
public function Field($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');

View File

@ -31,6 +31,8 @@ class DatalessField extends FormField {
/**
* Returns the field's representation in the form.
* For dataless fields, this defaults to $Field.
*
* @return HTMLText
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);

View File

@ -88,7 +88,10 @@ class DatetimeField extends FormField {
return $this;
}
/**
* @param array $properties
* @return HTMLText
*/
public function FieldHolder($properties = array()) {
$config = array(
'datetimeorder' => $this->getConfig('datetimeorder'),
@ -100,14 +103,19 @@ class DatetimeField extends FormField {
return parent::FieldHolder($properties);
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
Requirements::css(FRAMEWORK_DIR . '/css/DatetimeField.css');
$tzField = ($this->getConfig('usertimezone')) ? $this->timezoneField->FieldHolder() : '';
return $this->dateField->FieldHolder() .
return DBField::create_field('HTMLText', $this->dateField->FieldHolder() .
$this->timeField->FieldHolder() .
$tzField .
'<div class="clear"><!-- --></div>';
'<div class="clear"><!-- --></div>'
);
}
/**

View File

@ -128,6 +128,10 @@ class DropdownField extends FormField {
parent::__construct($name, ($title===null) ? $name : $title, $value, $form);
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
$source = $this->getSource();
$options = array();

View File

@ -83,6 +83,10 @@ class FileField extends FormField {
parent::__construct($name, $title, $value);
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
$properties = array_merge($properties, array(
'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize()

View File

@ -49,7 +49,7 @@ class FormAction extends FormField {
public function __construct($action, $title = "", $form = null) {
$this->action = "action_$action";
$this->setForm($form);
parent::__construct($this->action, $title);
}
@ -74,6 +74,10 @@ class FormAction extends FormField {
return $this;
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
$properties = array_merge(
$properties,
@ -87,6 +91,10 @@ class FormAction extends FormField {
return parent::Field($properties);
}
/**
* @param array $properties
* @return HTMLText
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);
}

View File

@ -10,7 +10,7 @@ class HiddenField extends FormField {
/**
* @param array $properties
*
* @return string
* @return HTMLText
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);

View File

@ -27,14 +27,25 @@ class InlineFormAction extends FormField {
return $this->castedCopy('InlineFormAction_ReadOnly');
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
if($this->includeDefaultJS) {
Requirements::javascriptTemplate(FRAMEWORK_DIR . '/javascript/InlineFormAction.js',
array('ID'=>$this->id()));
}
return "<input type=\"submit\" name=\"action_{$this->name}\" value=\"{$this->title}\" id=\"{$this->id()}\""
. " class=\"action{$this->extraClass}\" />";
return DBField::create_field(
'HTMLText',
FormField::create('input', array(
'name' => sprintf('action_%s', $this->getName()),
'value' => $this->title,
'id' => $this->ID(),
'class' => sprintf('action%s', $this->extraClass),
))
);
}
public function Title() {
@ -61,9 +72,21 @@ class InlineFormAction_ReadOnly extends FormField {
protected $readonly = true;
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
return "<input type=\"submit\" name=\"action_{$this->name}\" value=\"{$this->title}\" id=\"{$this->id()}\""
. " disabled=\"disabled\" class=\"action disabled$this->extraClass\" />";
return DBField::create_field('HTMLText',
FormField::create_tag('input', array(
'type' => 'submit',
'name' => sprintf('action_%s', $this->name),
'value' => $this->title,
'id' => $this->id(),
'disabled' => 'disabled',
'class' => 'action disabled ' . $this->extraClass,
))
);
}
public function Title() {

View File

@ -41,13 +41,16 @@ class MoneyField extends FormField {
}
/**
* @return string
* @param array
* @return HTMLText
*/
public function Field($properties = array()) {
return "<div class=\"fieldgroup\">" .
return DBField::create_field('HTMLText',
"<div class=\"fieldgroup\">" .
"<div class=\"fieldgroup-field\">" . $this->fieldCurrency->SmallFieldHolder() . "</div>" .
"<div class=\"fieldgroup-field\">" . $this->fieldAmount->SmallFieldHolder() . "</div>" .
"</div>";
"</div>"
);
}
/**

View File

@ -42,7 +42,6 @@ class NullableField extends FormField {
*/
protected $isNullLabel;
/**
* Create a new nullable field
*
@ -103,7 +102,7 @@ class NullableField extends FormField {
/**
* @param array $properties
*
* @return string
* @return HTMLText
*/
public function Field($properties = array()) {
if($this->isReadonly()) {
@ -114,12 +113,12 @@ class NullableField extends FormField {
$nullableCheckbox->setValue(is_null($this->dataValue()));
return sprintf(
return DBField::create_field('HTMLText', sprintf(
'%s %s&nbsp;<span>%s</span>',
$this->valueField->Field(),
$nullableCheckbox->Field(),
$this->getIsNullLabel()
);
));
}
/**

View File

@ -27,6 +27,10 @@ class PhoneNumberField extends FormField {
parent::__construct($name, $title, $value);
}
/**
* @param array $properties
* @return FieldGroup|HTMLText
*/
public function Field($properties = array()) {
$fields = new FieldGroup( $this->name );
$fields->setID("{$this->name}_Holder");

View File

@ -36,6 +36,10 @@ class ReadonlyField extends FormField {
return clone $this;
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
// Include a hidden field in the HTML
if($this->includeHiddenField && $this->readonly) {

View File

@ -204,7 +204,7 @@ class TreeDropdownField extends FormField {
}
/**
* @return string
* @return HTMLText
*/
public function Field($properties = array()) {
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');

View File

@ -279,7 +279,7 @@ class GridField extends FormField {
*
* @param array $properties
*
* @return string
* @return HTMLText
*/
public function FieldHolder($properties = array()) {
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
@ -501,11 +501,11 @@ class GridField extends FormField {
$header . "\n" . $footer . "\n" . $body
);
return FormField::create_tag(
return DBField::create_field('HTMLText', FormField::create_tag(
'fieldset',
$fieldsetAttributes,
$content['before'] . $table . $content['after']
);
));
}
/**
@ -589,7 +589,7 @@ class GridField extends FormField {
/**
* @param array $properties
*
* @return string
* @return HTMLText
*/
public function Field($properties = array()) {
return $this->FieldHolder($properties);

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

@ -372,6 +372,8 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
/**
* Return a new instance of the list with an added filter
*
* @param array $filterArray
*/
public function addFilter($filterArray) {
$list = $this;

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

@ -122,18 +122,6 @@ class Image extends File implements Flushable {
return $fields;
}
/**
* An image exists if it has a filename.
* Does not do any filesystem checks.
*
* @return boolean
*/
public function exists() {
if(isset($this->record["Filename"])) {
return true;
}
}
/**
* Return an XHTML img tag for this Image,
* or NULL if the image file doesn't exist on the filesystem.
@ -141,7 +129,7 @@ class Image extends File implements Flushable {
* @return string
*/
public function getTag() {
if(file_exists(Director::baseFolder() . '/' . $this->Filename)) {
if($this->exists()) {
$url = $this->getURL();
$title = ($this->Title) ? $this->Title : $this->Filename;
if($this->Title) {
@ -229,7 +217,7 @@ class Image extends File implements Flushable {
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->getWidth();
$heightRatio = $height / $this->getHeight();
if( $widthRatio < $heightRatio ) {
// Target is higher aspect ratio than image, so check width
if($this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')) return $this;
@ -257,7 +245,7 @@ class Image extends File implements Flushable {
/**
* Proportionally scale down this image if it is wider or taller than the specified dimensions.
* Similar to Fit but without up-sampling. Use in templates with $FitMax.
*
*
* @uses Image::Fit()
* @param integer $width The maximum width of the output image
* @param integer $height The maximum height of the output image
@ -266,7 +254,7 @@ class Image extends File implements Flushable {
public function FitMax($width, $height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width && $this->getHeight() <= $height) return $this->Fit($this->getWidth(),$this->getHeight());
return $this->getWidth() > $width || $this->getHeight() > $height
? $this->Fit($width,$height)
: $this;
@ -300,7 +288,7 @@ class Image extends File implements Flushable {
}
/**
* Crop this image to the aspect ratio defined by the specified width and height,
* Crop this image to the aspect ratio defined by the specified width and height,
* then scale down the image to those dimensions if it exceeds them.
* Similar to Fill but without up-sampling. Use in templates with $FillMax.
*
@ -312,13 +300,13 @@ class Image extends File implements Flushable {
public function FillMax($width, $height) {
// Prevent divide by zero on missing/blank file
if(!$this->getWidth() || !$this->getHeight()) return null;
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->isSize($width, $height)) return $this->Fill($width, $height);
// Is the image already the correct size?
if ($this->isSize($width, $height)) return $this;
// If not, make sure the image isn't upsampled
$imageRatio = $this->getWidth() / $this->getHeight();
$cropRatio = $width / $height;
@ -326,7 +314,7 @@ class Image extends File implements Flushable {
if ($cropRatio < $imageRatio && $this->getHeight() < $height) return $this->Fill($this->getHeight()*$cropRatio, $this->getHeight());
// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
if ($this->getWidth() < $width) return $this->Fill($this->getWidth(), $this->getWidth()/$cropRatio);
return $this->Fill($width, $height);
}
@ -379,7 +367,7 @@ class Image extends File implements Flushable {
}
/**
* Proportionally scale down this image if it is wider than the specified width.
* Proportionally scale down this image if it is wider than the specified width.
* Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
*
* @uses Image::ScaleWidth()
@ -389,7 +377,7 @@ class Image extends File implements Flushable {
public function ScaleMaxWidth($width) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->ScaleWidth($this->getWidth());
return $this->getWidth() > $width
? $this->ScaleWidth($width)
: $this;
@ -419,7 +407,7 @@ class Image extends File implements Flushable {
}
/**
* Proportionally scale down this image if it is taller than the specified height.
* Proportionally scale down this image if it is taller than the specified height.
* Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
*
* @uses Image::ScaleHeight()
@ -429,7 +417,7 @@ class Image extends File implements Flushable {
public function ScaleMaxHeight($height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->ScaleHeight($this->getHeight());
return $this->getHeight() > $height
? $this->ScaleHeight($height)
: $this;
@ -446,7 +434,7 @@ class Image extends File implements Flushable {
public function CropWidth($width) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->Fill($this->getWidth(), $this->getHeight());
return $this->getWidth() > $width
? $this->Fill($width, $this->getHeight())
: $this;
@ -463,7 +451,7 @@ class Image extends File implements Flushable {
public function CropHeight($height) {
// Temporary $force_resample support for 3.x, to be removed in 4.0
if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->Fill($this->getWidth(), $this->getHeight());
return $this->getHeight() > $height
? $this->Fill($this->getWidth(), $height)
: $this;
@ -684,7 +672,7 @@ class Image extends File implements Flushable {
public function getFormattedImage($format) {
$args = func_get_args();
if($this->ID && $this->Filename && Director::fileExists($this->Filename)) {
if($this->exists()) {
$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
if(!file_exists(Director::baseFolder()."/".$cacheFile) || self::$flush) {
@ -950,8 +938,8 @@ class Image extends File implements Flushable {
public function getDimensions($dim = "string") {
if($this->getField('Filename')) {
$imagefile = Director::baseFolder() . '/' . $this->getField('Filename');
if(file_exists($imagefile)) {
$imagefile = $this->getFullPath();
if($this->exists()) {
$size = getimagesize($imagefile);
return ($dim === "string") ? "$size[0]x$size[1]" : $size[$dim];
} else {
@ -1029,6 +1017,16 @@ class Image_Cached extends Image {
$this->Filename = $filename;
}
/**
* Override the parent's exists method becuase the ID is explicitly set to -1 on a cached image we can't use the
* default check
*
* @return bool Whether the cached image exists
*/
public function exists() {
return file_exists($this->getFullPath());
}
public function getRelativePath() {
return $this->getField('Filename');
}

View File

@ -12,25 +12,13 @@ class MySQLSchemaManager extends DBSchemaManager {
* Identifier for this schema, used for configuring schema-specific table
* creation options
*/
const ID = 'MySQL';
const ID = 'MySQLDatabase';
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
$fieldSchemas = $indexSchemas = "";
if (!empty($options[self::ID])) {
$addOptions = $options[self::ID];
} elseif (!empty($options[get_class($this)])) {
Deprecation::notice(
'3.2',
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
);
$addOptions = $options[get_class($this)];
} elseif (!empty($options[get_parent_class($this)])) {
Deprecation::notice(
'3.2',
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
);
$addOptions = $options[get_parent_class($this)];
} else {
$addOptions = "ENGINE=InnoDB";
}

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

@ -1326,6 +1326,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
// Members are displayed within group edit form in SecurityAdmin
$fields->removeByName('Groups');
// Members shouldn't be able to directly view/edit logged passwords
$fields->removeByName('LoggedPasswords');
if(Permission::check('EDIT_PERMISSIONS')) {
$groupsMap = array();
foreach(Group::get() as $group) {

View File

@ -71,6 +71,10 @@ class PermissionCheckboxSetField extends FormField {
return $this->hiddenPermissions;
}
/**
* @param array $properties
* @return HTMLText
*/
public function Field($properties = array()) {
Requirements::css(FRAMEWORK_DIR . '/css/CheckboxSetField.css');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/PermissionCheckboxSetField.js');
@ -227,7 +231,8 @@ class PermissionCheckboxSetField extends FormField {
}
}
if($this->readonly) {
return "<ul id=\"{$this->id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
return DBField::create_field('HTMLText',
"<ul id=\"{$this->id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
"<li class=\"help\">" .
_t(
'Permissions.UserPermissionsIntro',
@ -236,11 +241,14 @@ class PermissionCheckboxSetField extends FormField {
) .
"</li>" .
$options .
"</ul>\n";
"</ul>\n"
);
} else {
return "<ul id=\"{$this->id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
return DBField::create_field('HTMLText',
"<ul id=\"{$this->id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
$options .
"</ul>\n";
"</ul>\n"
);
}
}

View File

@ -1,4 +1,4 @@
<ul id="$HolderID" class="$extraClass">
<ul $AttributesHTML>
<% if $Options.Count %>
<% loop $Options %>
<li class="$Class">

View File

@ -1,4 +1,4 @@
<ul id="$HolderID" class="$extraClass">
<ul $AttributesHTML>
<% loop $Options %>
<li class="$Class">
<input id="$ID" class="radio" name="$Name" type="radio" value="$Value"<% if $isChecked %> checked<% end_if %><% if $isDisabled %> disabled<% end_if %> />

View File

@ -11,6 +11,18 @@ class ControllerTest extends FunctionalTest {
'ControllerTest_AccessBaseControllerExtension'
)
);
protected $depSettings = null;
public function setUp() {
parent::setUp();
$this->depSettings = Deprecation::dump_settings();
}
public function tearDown() {
Deprecation::restore_settings($this->depSettings);
parent::tearDown();
}
public function testDefaultAction() {
/* For a controller with a template, the default action will simple run that template. */
@ -208,6 +220,7 @@ class ControllerTest extends FunctionalTest {
* @expectedExceptionMessage Wildcards (*) are no longer valid
*/
public function testWildcardAllowedActions() {
Deprecation::set_enabled(true);
$this->get('ControllerTest_AccessWildcardSecuredController');
}

View File

@ -7,6 +7,26 @@
*/
class HTTPTest extends FunctionalTest {
public function testAddCacheHeaders() {
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
$response = new SS_HTTPResponse($body, 200);
$this->assertEmpty($response->getHeader('Cache-Control'));
HTTP::set_cache_age(30);
HTTP::add_cache_headers($response);
$this->assertNotEmpty($response->getHeader('Cache-Control'));
Config::inst()->update('Director', 'environment_type', 'dev');
HTTP::add_cache_headers($response);
$this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
Config::inst()->update('Director', 'environment_type', 'live');
HTTP::add_cache_headers($response);
$this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
$this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
}
/**
* Tests {@link HTTP::getLinksIn()}
*/

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

@ -82,6 +82,19 @@ class ConfigTest_TestNest extends Object implements TestOnly {
}
class ConfigTest extends SapphireTest {
protected $depSettings = null;
public function setUp() {
parent::setUp();
$this->depSettings = Deprecation::dump_settings();
Deprecation::set_enabled(false);
}
public function tearDown() {
Deprecation::restore_settings($this->depSettings);
parent::tearDown();
}
public function testNest() {
@ -282,12 +295,6 @@ class ConfigTest extends SapphireTest {
}
public function testLRUDiscarding() {
$depSettings = Deprecation::dump_settings();
Deprecation::restore_settings(array(
'level' => false,
'version' => false,
'moduleVersions' => false,
));
$cache = new ConfigTest_Config_LRU();
for ($i = 0; $i < Config_LRU::SIZE*2; $i++) $cache->set($i, $i);
$this->assertEquals(
@ -301,16 +308,9 @@ class ConfigTest extends SapphireTest {
Config_LRU::SIZE, count($cache->indexing),
'Heterogenous usage gives sufficient discarding'
);
Deprecation::restore_settings($depSettings);
}
public function testLRUCleaning() {
$depSettings = Deprecation::dump_settings();
Deprecation::restore_settings(array(
'level' => false,
'version' => false,
'moduleVersions' => false,
));
$cache = new ConfigTest_Config_LRU();
for ($i = 0; $i < Config_LRU::SIZE; $i++) $cache->set($i, $i);
$this->assertEquals(Config_LRU::SIZE, count($cache->indexing));
@ -327,7 +327,6 @@ class ConfigTest extends SapphireTest {
$cache->clean('Bar');
$this->assertEquals(0, count($cache->indexing), 'Clean items with any single matching tag');
$this->assertFalse($cache->get(1), 'Clean items with any single matching tag');
Deprecation::restore_settings($depSettings);
}
}

View File

@ -15,12 +15,16 @@ class DeprecationTest extends SapphireTest {
static $originalVersionInfo;
public function setUp() {
parent::setUp();
self::$originalVersionInfo = Deprecation::dump_settings();
Deprecation::$notice_level = E_USER_NOTICE;
Deprecation::set_enabled(true);
}
public function tearDown() {
Deprecation::restore_settings(self::$originalVersionInfo);
parent::tearDown();
}
public function testLesserVersionTriggersNoNotice() {

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

@ -253,23 +253,49 @@ class UploadFieldTest extends FunctionalTest {
// two should work and the third will fail.
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
$record->HasManyFilesMaxTwo()->removeAll();
$this->assertCount(0, $record->HasManyFilesMaxTwo());
// Get references for each file to upload
$file1 = $this->objFromFixture('File', 'file1');
$file2 = $this->objFromFixture('File', 'file2');
$file3 = $this->objFromFixture('File', 'file3');
$this->assertTrue($file1->exists());
$this->assertTrue($file2->exists());
$this->assertTrue($file3->exists());
// Write the first element, should be okay.
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID));
$this->assertEmpty($response['errors']);
$this->assertCount(1, $record->HasManyFilesMaxTwo());
$this->assertContains($file1->ID, $record->HasManyFilesMaxTwo()->getIDList());
$record->HasManyFilesMaxTwo()->removeAll();
$this->assertCount(0, $record->HasManyFilesMaxTwo());
$this->assertTrue($file1->exists());
$this->assertTrue($file2->exists());
$this->assertTrue($file3->exists());
// Write the second element, should be okay.
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID));
$this->assertEmpty($response['errors']);
$this->assertCount(2, $record->HasManyFilesMaxTwo());
$this->assertContains($file1->ID, $record->HasManyFilesMaxTwo()->getIDList());
$this->assertContains($file2->ID, $record->HasManyFilesMaxTwo()->getIDList());
$record->HasManyFilesMaxTwo()->removeAll();
$this->assertCount(0, $record->HasManyFilesMaxTwo());
$this->assertTrue($file1->exists());
$this->assertTrue($file2->exists());
$this->assertTrue($file3->exists());
// Write the third element, should result in error.
$response = $this->mockUploadFileIDs('HasManyFilesMaxTwo', array($file1->ID, $file2->ID, $file3->ID));
$this->assertNotEmpty($response['errors']);
$this->assertCount(0, $record->HasManyFilesMaxTwo());
}
/**
@ -948,22 +974,21 @@ class UploadFieldTest extends FunctionalTest {
}
public function setUp() {
Config::inst()->update('File', 'update_filesystem', false);
parent::setUp();
if(!file_exists(ASSETS_PATH)) mkdir(ASSETS_PATH);
/* Create a test folders for each of the fixture references */
$folderIDs = $this->allFixtureIDs('Folder');
foreach($folderIDs as $folderID) {
$folder = DataObject::get_by_id('Folder', $folderID);
if(!file_exists(BASE_PATH."/$folder->Filename")) mkdir(BASE_PATH."/$folder->Filename");
$folders = Folder::get()->byIDs($this->allFixtureIDs('Folder'));
foreach($folders as $folder) {
if(!file_exists($folder->getFullPath())) mkdir($folder->getFullPath());
}
/* Create a test files for each of the fixture references */
$fileIDs = $this->allFixtureIDs('File');
foreach($fileIDs as $fileID) {
$file = DataObject::get_by_id('File', $fileID);
$fh = fopen(BASE_PATH."/$file->Filename", "w");
$files = File::get()->byIDs($this->allFixtureIDs('File'));
foreach($files as $file) {
$fh = fopen($file->getFullPath(), "w");
fwrite($fh, str_repeat('x',1000000));
fclose($fh);
}

View File

@ -3,48 +3,48 @@ Folder:
Name: UploadFieldTest
folder1-subfolder1:
Name: subfolder1
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
File:
file1:
Title: File1
Filename: assets/UploadFieldTest/file1.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file2:
Title: File2
Filename: assets/UploadFieldTest/file2.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file3:
Title: File3
Filename: assets/UploadFieldTest/file3.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file4:
Title: File4
Filename: assets/UploadFieldTest/file4.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file5:
Title: File5
Filename: assets/UploadFieldTest/file5.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file-noview:
Title: noview.txt
Name: noview.txt
Filename: assets/UploadFieldTest/noview.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file-noedit:
Title: noedit.txt
Name: noedit.txt
Filename: assets/UploadFieldTest/noedit.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file-nodelete:
Title: nodelete.txt
Name: nodelete.txt
Filename: assets/UploadFieldTest/nodelete.txt
ParentID: =>Folder.folder1
Parent: =>Folder.folder1
file-subfolder:
Title: file-subfolder.txt
Name: file-subfolder.txt
Filename: assets/UploadFieldTest/subfolder1/file-subfolder.txt
ParentID: =>Folder.folder1-subfolder1
Parent: =>Folder.folder1-subfolder1
UploadFieldTest_Record:
record1:
Title: Record 1

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.
*