mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge remote-tracking branch 'origin/3.2' into 3
Conflicts: css/AssetUploadField.css
This commit is contained in:
commit
e0a560051e
@ -7,10 +7,6 @@ addons:
|
||||
packages:
|
||||
- tidy
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
php:
|
||||
- 5.4
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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); }
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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.|
|
||||
|
@ -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]
|
@ -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
|
||||
|
@ -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.* |
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
@ -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>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ class HiddenField extends FormField {
|
||||
/**
|
||||
* @param array $properties
|
||||
*
|
||||
* @return string
|
||||
* @return HTMLText
|
||||
*/
|
||||
public function FieldHolder($properties = array()) {
|
||||
return $this->Field($properties);
|
||||
|
@ -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() {
|
||||
|
@ -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>"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 <span>%s</span>',
|
||||
$this->valueField->Field(),
|
||||
$nullableCheckbox->Field(),
|
||||
$this->getIsNullLabel()
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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");
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
47
main.php
47
main.php
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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
40
search/filters/FulltextFilter.php
Normal file → Executable 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<ul id="$HolderID" class="$extraClass">
|
||||
<ul $AttributesHTML>
|
||||
<% if $Options.Count %>
|
||||
<% loop $Options %>
|
||||
<li class="$Class">
|
||||
|
@ -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 %> />
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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()}
|
||||
*/
|
||||
|
@ -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')
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 {
|
||||
|
105
tests/search/FulltextFilterTest.php
Executable file
105
tests/search/FulltextFilterTest.php
Executable 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",
|
||||
);
|
||||
|
||||
}
|
19
tests/search/FulltextFilterTest.yml
Normal file
19
tests/search/FulltextFilterTest.yml
Normal 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: ''
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user