From b617ef1abdd82a52b5f80e9741112b0eac086a55 Mon Sep 17 00:00:00 2001 From: ielmin Date: Thu, 26 Mar 2015 13:20:21 +1100 Subject: [PATCH 01/22] Hardcoded http:// cause browser warnings --- admin/code/LeftAndMain.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 268229dd0..169db1928 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -76,7 +76,7 @@ class LeftAndMain extends Controller implements PermissionProvider { * @config * @var string */ - private static $help_link = 'http://userhelp.silverstripe.org/framework/en/3.1'; + private static $help_link = '//userhelp.silverstripe.org/framework/en/3.1'; /** * @var array @@ -1595,7 +1595,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 From 1cca37c9082ef53f02633d1bdac27f4a815d4208 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Mon, 4 May 2015 15:10:16 +0100 Subject: [PATCH 02/22] FIX: File::getFileType() was case sensitive (fixes #3631) --- filesystem/File.php | 2 +- tests/filesystem/FileTest.php | 3 +++ tests/filesystem/FileTest.yml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/filesystem/File.php b/filesystem/File.php index ca0b51c9e..8f5124220 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -808,7 +808,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'; } diff --git a/tests/filesystem/FileTest.php b/tests/filesystem/FileTest.php index 1466810a8..750479ad5 100644 --- a/tests/filesystem/FileTest.php +++ b/tests/filesystem/FileTest.php @@ -249,6 +249,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'); diff --git a/tests/filesystem/FileTest.yml b/tests/filesystem/FileTest.yml index 636339f15..47e5190aa 100644 --- a/tests/filesystem/FileTest.yml +++ b/tests/filesystem/FileTest.yml @@ -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: From 40c5b8b6758676a3e2a5daf3c438a7720c49baaf Mon Sep 17 00:00:00 2001 From: micmania1 Date: Sun, 25 May 2014 01:46:07 +0100 Subject: [PATCH 03/22] FIX FulltextFilter did not work and was not usable --- search/filters/FulltextFilter.php | 42 ++++++++++++++++++-- search/filters/SearchFilter.php | 7 +++- tests/search/FulltextFilterTest.php | 61 +++++++++++++++++++++++++++++ tests/search/FulltextFilterTest.yml | 16 ++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) mode change 100644 => 100755 search/filters/FulltextFilter.php create mode 100755 tests/search/FulltextFilterTest.php create mode 100644 tests/search/FulltextFilterTest.yml diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php old mode 100644 new mode 100755 index 36b77b4c2..ec98e2f63 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -17,17 +17,17 @@ * database table, using the {$indexes} hash in your DataObject subclass: * * - * static $indexes = array( + * private static $indexes = array( * 'SearchFields' => 'fulltext(Name, Title, Description)' * ); * * - * @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); return $query->where(sprintf( "MATCH (%s) AGAINST ('%s')", $this->getDbName(), @@ -36,6 +36,7 @@ class FulltextFilter extends SearchFilter { } protected function excludeOne(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); return $query->where(sprintf( "NOT MATCH (%s) AGAINST ('%s')", $this->getDbName(), @@ -46,4 +47,39 @@ 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 + * + * MyDataObject::get()->filter('SearchFields:fulltext', 'search term') + * + * + * @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\ \((.+)\)$/i', $index, $matches)) { + return $matches[1]; + } else { + throw new Exception("Invalid fulltext index format for '" . $this->getName() + . "' on '" . $this->model . "'"); + return; + } + } + return $columns; + } + + throw new Exception($this->getName() . ' is not a fulltext index on ' . $this->model . '.'); + } + } diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index e09e97dd1..b4efdfc9a 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -166,6 +166,11 @@ abstract class SearchFilter extends Object { return $this->name; } + // Ensure that we're dealing with a DataObject. + if (!is_subclass_of($this->model, 'DataObject')) { + throw new Exception("Model supplied to " . get_class($this) . " should be an instance of DataObject."); + } + $candidateClass = ClassInfo::table_for_object_field( $this->model, $this->name @@ -178,7 +183,7 @@ abstract class SearchFilter extends Object { return '"' . implode('"."', $parts) . '"'; } - return "\"$candidateClass\".\"$this->name\""; + return "\"{$candidateClass}\".\"{$this->name}\""; } /** diff --git a/tests/search/FulltextFilterTest.php b/tests/search/FulltextFilterTest.php new file mode 100755 index 000000000..49b75911d --- /dev/null +++ b/tests/search/FulltextFilterTest.php @@ -0,0 +1,61 @@ +assertEquals(3, $baseQuery->count(), "FulltextDataObject 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()); + + // Edgecase + $this->setExpectedException("Exception"); + $search = $baseQuery->exclude("Madeup:fulltext", "SilverStripe"); + } else { + $this->markTestSkipped("FulltextFilter only supports MySQL syntax."); + } + } + +} + + +class FulltextDataObject extends DataObject { + + private static $db = array( + "ColumnA" => "Varchar(255)", + "ColumnB" => "HTMLText", + "ColumnC" => "Varchar(255)", + "ColumnD" => "HTMLText", + ); + + private static $indexes = array( + 'SearchFields' => array( + 'type' => 'fulltext', + 'name' => 'SearchFields', + 'value' => '"ColumnA", "ColumnB"', + ), + 'OtherSearchFields' => 'fulltext ("ColumnC", "ColumnD")', + ); + + private static $create_table_options = array( + "MySQLDatabase" => "ENGINE=MyISAM", + ); + +} \ No newline at end of file diff --git a/tests/search/FulltextFilterTest.yml b/tests/search/FulltextFilterTest.yml new file mode 100644 index 000000000..b3be2215b --- /dev/null +++ b/tests/search/FulltextFilterTest.yml @@ -0,0 +1,16 @@ +FulltextDataObject: + object1: + ColumnA: 'SilverStripe' + CluumnB:

Some content about SilverStripe.

+ ColumnC: 'SilverStripe' + ColumnD: '

Some content about SilverStripe.

+ object2: + ColumnA: 'Test Row' + ColumnB: '

Some information about this test row.

' + ColumnC: 'Test Row' + ColumnD: '

Some information about this test row.

' + object3: + ColumnA: 'Fulltext Search' + ColumnB: '

Testing fulltext search.

' + ColumnC: 'Fulltext Search + ColumnD: '

Testing fulltext search.

' \ No newline at end of file From 782c4cbf6f5cde2fa4d45cdbd17552773a67f88f Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 29 Jan 2015 12:48:46 +1300 Subject: [PATCH 04/22] API Enable single-column fulltext filter search as fallback --- .../12_Search/02_FulltextSearch.md | 37 ++++++++++++ search/filters/FulltextFilter.php | 10 ++-- search/filters/SearchFilter.php | 37 ++++++------ tests/search/FulltextFilterTest.php | 60 ++++++++++++++++--- tests/search/FulltextFilterTest.yml | 13 ++-- 5 files changed, 120 insertions(+), 37 deletions(-) diff --git a/docs/en/02_Developer_Guides/12_Search/02_FulltextSearch.md b/docs/en/02_Developer_Guides/12_Search/02_FulltextSearch.md index 11fdb7c20..82153ca2d 100644 --- a/docs/en/02_Developer_Guides/12_Search/02_FulltextSearch.md +++ b/docs/en/02_Developer_Guides/12_Search/02_FulltextSearch.md @@ -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. +### 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] \ No newline at end of file diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php index ec98e2f63..36fbf222e 100755 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -68,18 +68,16 @@ class FulltextFilter extends SearchFilter { } else { // Parse a fulltext string (eg. fulltext ("ColumnA", "ColumnB")) to figure out which columns // we need to search. - if(preg_match('/^fulltext\ \((.+)\)$/i', $index, $matches)) { + 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; } } - return $columns; - } - - throw new Exception($this->getName() . ' is not a fulltext index on ' . $this->model . '.'); + } + + return parent::getDbName(); } } diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index b4efdfc9a..c1dbd90c5 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -9,27 +9,27 @@ * @subpackage search */ abstract class SearchFilter extends Object { - + /** * @var string Classname of the inspected {@link DataObject} */ protected $model; - + /** * @var string */ protected $name; - + /** * @var string */ protected $fullName; - + /** * @var mixed */ protected $value; - + /** * @var array */ @@ -41,7 +41,7 @@ abstract class SearchFilter extends Object { * {@link applyRelation()}. */ protected $relation; - + /** * @param string $fullName Determines the name of the field, as well as the searched database * column. Can contain a relation name in dot notation, which will automatically join @@ -58,7 +58,7 @@ abstract class SearchFilter extends Object { $this->value = $value; $this->setModifiers($modifiers); } - + /** * Called by constructor to convert a string pathname into * a well defined relationship sequence. @@ -74,7 +74,7 @@ abstract class SearchFilter extends Object { $this->name = $name; } } - + /** * Set the root model class to be selected by this * search query. @@ -84,7 +84,7 @@ abstract class SearchFilter extends Object { public function setModel($className) { $this->model = $className; } - + /** * Set the current value to be filtered on. * @@ -93,7 +93,7 @@ abstract class SearchFilter extends Object { public function setValue($value) { $this->value = $value; } - + /** * Accessor for the current value to be filtered on. * Caution: Data is not escaped. @@ -121,7 +121,7 @@ abstract class SearchFilter extends Object { public function getModifiers() { return $this->modifiers; } - + /** * The original name of the field. * @@ -137,7 +137,7 @@ abstract class SearchFilter extends Object { public function setName($name) { $this->name = $name; } - + /** * The full name passed to the constructor, * including any (optional) relations in dot notation. @@ -154,7 +154,7 @@ abstract class SearchFilter extends Object { public function setFullName($name) { $this->fullName = $name; } - + /** * Normalizes the field name to table mapping. * @@ -168,7 +168,9 @@ abstract class SearchFilter extends Object { // Ensure that we're dealing with a DataObject. if (!is_subclass_of($this->model, 'DataObject')) { - throw new Exception("Model supplied to " . get_class($this) . " should be an instance of DataObject."); + throw new InvalidArgumentException( + "Model supplied to " . get_class($this) . " should be an instance of DataObject." + ); } $candidateClass = ClassInfo::table_for_object_field( @@ -183,9 +185,9 @@ abstract class SearchFilter extends Object { return '"' . implode('"."', $parts) . '"'; } - return "\"{$candidateClass}\".\"{$this->name}\""; + return sprintf('"%s"."%s"', $candidateClass, $this->name); } - + /** * Return the value of the field as processed by the DBField class * @@ -200,7 +202,6 @@ abstract class SearchFilter extends Object { return $dbField->RAW(); } - /** * Apply filter criteria to a SQL query. * @@ -272,7 +273,7 @@ abstract class SearchFilter extends Object { protected function excludeMany(DataQuery $query) { throw new InvalidArgumentException(get_class($this) . " can't be used to filter by a list of items."); } - + /** * Determines if a field has a value, * and that the filter should be applied. diff --git a/tests/search/FulltextFilterTest.php b/tests/search/FulltextFilterTest.php index 49b75911d..efca15c6e 100755 --- a/tests/search/FulltextFilterTest.php +++ b/tests/search/FulltextFilterTest.php @@ -1,14 +1,17 @@ assertEquals(3, $baseQuery->count(), "FulltextDataObject count does not match."); + $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'); @@ -25,24 +28,64 @@ class FulltextFilterTest extends SapphireTest { $search = $baseQuery->exclude("OtherSearchFields:fulltext", "SilverStripe"); $this->assertEquals(2, $search->count()); - // Edgecase - $this->setExpectedException("Exception"); - $search = $baseQuery->exclude("Madeup:fulltext", "SilverStripe"); + // 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 FulltextDataObject extends DataObject { +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( @@ -52,6 +95,7 @@ class FulltextDataObject extends DataObject { 'value' => '"ColumnA", "ColumnB"', ), 'OtherSearchFields' => 'fulltext ("ColumnC", "ColumnD")', + 'SingleIndex' => 'fulltext ("ColumnE")' ); private static $create_table_options = array( diff --git a/tests/search/FulltextFilterTest.yml b/tests/search/FulltextFilterTest.yml index b3be2215b..1f59ca081 100644 --- a/tests/search/FulltextFilterTest.yml +++ b/tests/search/FulltextFilterTest.yml @@ -1,16 +1,19 @@ -FulltextDataObject: +FulltextFilterTest_DataObject: object1: ColumnA: 'SilverStripe' - CluumnB:

Some content about SilverStripe.

+ CluumnB: '

Some content about SilverStripe.

' ColumnC: 'SilverStripe' - ColumnD: '

Some content about SilverStripe.

+ ColumnD: '

Some content about SilverStripe.

' + ColumnE: 'Dragons be here' object2: ColumnA: 'Test Row' ColumnB: '

Some information about this test row.

' ColumnC: 'Test Row' ColumnD: '

Some information about this test row.

' + ColumnE: 'No' object3: ColumnA: 'Fulltext Search' ColumnB: '

Testing fulltext search.

' - ColumnC: 'Fulltext Search - ColumnD: '

Testing fulltext search.

' \ No newline at end of file + ColumnC: 'Fulltext Search' + ColumnD: '

Testing fulltext search.

' + ColumnE: '' From 71a14c30352e69e4c0ac59e5ea72e1da0c79009b Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 1 Jan 2015 12:01:01 +1300 Subject: [PATCH 05/22] BUG Prevent url= querystring argument override --- main.php | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/main.php b/main.php index e294530cb..854d5071f 100644 --- a/main.php +++ b/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=.*)$/', $_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; } } From 7ff131daa76d345cff90410469accdcca9049cf1 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 1 Apr 2015 14:31:55 +1300 Subject: [PATCH 06/22] BUG Fix default casted (boolean)false evaluating to true in templates --- model/fieldtypes/StringField.php | 4 +- tests/model/StringFieldTest.php | 16 ++++++++ ...iewerTestIncludeScopeInheritanceInclude.ss | 2 +- tests/view/SSViewerTest.php | 40 ++++++++++++++++--- view/SSViewer.php | 36 +++++++++-------- 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/model/fieldtypes/StringField.php b/model/fieldtypes/StringField.php index 1089becab..c4d34cf17 100644 --- a/model/fieldtypes/StringField.php +++ b/model/fieldtypes/StringField.php @@ -83,7 +83,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 } /** diff --git a/tests/model/StringFieldTest.php b/tests/model/StringFieldTest.php index 0dfb9499d..e2029cbea 100644 --- a/tests/model/StringFieldTest.php +++ b/tests/model/StringFieldTest.php @@ -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 { diff --git a/tests/templates/SSViewerTestIncludeScopeInheritanceInclude.ss b/tests/templates/SSViewerTestIncludeScopeInheritanceInclude.ss index 51b2dc190..0a67cba95 100644 --- a/tests/templates/SSViewerTestIncludeScopeInheritanceInclude.ss +++ b/tests/templates/SSViewerTestIncludeScopeInheritanceInclude.ss @@ -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 diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index c8ce9f2f3..56882db0f 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -46,17 +46,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( diff --git a/view/SSViewer.php b/view/SSViewer.php index d5fdb68bb..712a2e302 100644 --- a/view/SSViewer.php +++ b/view/SSViewer.php @@ -429,6 +429,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; @@ -512,32 +521,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. * From 54b0b1fd4e5ffedf87e1767b2f5572e48d88580c Mon Sep 17 00:00:00 2001 From: David Alexander Date: Thu, 18 Jun 2015 17:26:58 +1200 Subject: [PATCH 07/22] Update 02_Release_Process.md Typos. Spelling. 404 errors for framework and cms milestones links to github(line 13). --- docs/en/05_Contributing/02_Release_Process.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/en/05_Contributing/02_Release_Process.md b/docs/en/05_Contributing/02_Release_Process.md index 74f0805cd..9800a63e9 100644 --- a/docs/en/05_Contributing/02_Release_Process.md +++ b/docs/en/05_Contributing/02_Release_Process.md @@ -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,14 +53,14 @@ How to deprecate an API: * Add a `@deprecated` item to the docblock tag, with a `{@link }` 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 +* Deprecated APIs can be removed after developers have 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. @@ -77,8 +77,8 @@ Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::i return Env::is_dev(); } -This change could be committed to a minor release like *3.2.0*, and stays deprecated in all following minor releases -(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*) where it gets removed from the codebase. +This change could be committed to a minor release like *3.2.0*, and remains deprecated in all subsequent minor releases +(e.g. *3.3.0*, *3.4.0*), until a new major release (e.g. *4.0.0*), at which point it gets removed from the codebase. ## Security Releases @@ -99,7 +99,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 +109,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.* | From f6d60ca946ecfba7712fb9bc88f1d12361d572e7 Mon Sep 17 00:00:00 2001 From: David Alexander Date: Fri, 19 Jun 2015 07:51:16 +1200 Subject: [PATCH 08/22] Update 02_Release_Process.md Updated: deprecated API's removed only in MAJOR releases. --- docs/en/05_Contributing/02_Release_Process.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/en/05_Contributing/02_Release_Process.md b/docs/en/05_Contributing/02_Release_Process.md index 9800a63e9..92b3b19e1 100644 --- a/docs/en/05_Contributing/02_Release_Process.md +++ b/docs/en/05_Contributing/02_Release_Process.md @@ -60,10 +60,8 @@ How to deprecate an API: * 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 have 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()`: From 33d93c2a31b7b557510e8da9a6d9da2b901256c1 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sat, 20 Jun 2015 17:57:18 +0100 Subject: [PATCH 09/22] Fixing issues with HTTP cache control --- control/HTTP.php | 17 ++++++++--------- tests/control/HTTPTest.php | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/control/HTTP.php b/control/HTTP.php index 97dcb84f7..45abaec4f 100644 --- a/control/HTTP.php +++ b/control/HTTP.php @@ -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); diff --git a/tests/control/HTTPTest.php b/tests/control/HTTPTest.php index ffede9e32..42d6e2d88 100644 --- a/tests/control/HTTPTest.php +++ b/tests/control/HTTPTest.php @@ -7,6 +7,26 @@ */ class HTTPTest extends FunctionalTest { + public function testAddCacheHeaders() { + $body = "

Mysite

"; + $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()} */ From 560f9a6e39df4f09dfe4bed5978f6dcddc0bb299 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 10 Jul 2015 14:49:05 +1200 Subject: [PATCH 10/22] API respect custom attributes on OptionsetField and CheckboxSetField --- admin/scss/_actionTabs.scss | 4 ++-- css/AssetUploadField.css | 2 +- templates/forms/CheckboxSetField.ss | 2 +- templates/forms/OptionsetField.ss | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/admin/scss/_actionTabs.scss b/admin/scss/_actionTabs.scss index 661b5b475..966d5583e 100644 --- a/admin/scss/_actionTabs.scss +++ b/admin/scss/_actionTabs.scss @@ -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{ diff --git a/css/AssetUploadField.css b/css/AssetUploadField.css index 33e69e44c..1a4f66dbc 100644 --- a/css/AssetUploadField.css +++ b/css/AssetUploadField.css @@ -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: #5e5e5e; 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); } diff --git a/templates/forms/CheckboxSetField.ss b/templates/forms/CheckboxSetField.ss index 0826cdc17..f5210733e 100644 --- a/templates/forms/CheckboxSetField.ss +++ b/templates/forms/CheckboxSetField.ss @@ -1,4 +1,4 @@ -
    +
      <% if $Options.Count %> <% loop $Options %>
    • diff --git a/templates/forms/OptionsetField.ss b/templates/forms/OptionsetField.ss index d84d9829e..aa3f4cc5e 100644 --- a/templates/forms/OptionsetField.ss +++ b/templates/forms/OptionsetField.ss @@ -1,4 +1,4 @@ -
        +
          <% loop $Options %>
        • checked<% end_if %><% if $isDisabled %> disabled<% end_if %> /> From 914d734df004947eb869de2abb6fb2fc463be574 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Wed, 10 Jun 2015 10:27:54 +1200 Subject: [PATCH 11/22] API Disable deprecation notices by default --- dev/Deprecation.php | 66 +++++++++++++++---- .../03_Environment_Management.md | 1 + docs/en/04_Changelogs/3.2.0.md | 2 + docs/en/05_Contributing/02_Release_Process.md | 19 ++++++ tests/control/ControllerTest.php | 13 ++++ tests/core/ConfigTest.php | 27 ++++---- tests/dev/DeprecationTest.php | 4 ++ 7 files changed, 106 insertions(+), 26 deletions(-) diff --git a/dev/Deprecation.php b/dev/Deprecation.php index de70ab04b..b31aefdce 100644 --- a/dev/Deprecation.php +++ b/dev/Deprecation.php @@ -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']; } } diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md index b3f50ad7e..e915a003b 100644 --- a/docs/en/00_Getting_Started/03_Environment_Management.md +++ b/docs/en/00_Getting_Started/03_Environment_Management.md @@ -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.| diff --git a/docs/en/04_Changelogs/3.2.0.md b/docs/en/04_Changelogs/3.2.0.md index 97c8c37ce..a3206cfda 100644 --- a/docs/en/04_Changelogs/3.2.0.md +++ b/docs/en/04_Changelogs/3.2.0.md @@ -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 diff --git a/docs/en/05_Contributing/02_Release_Process.md b/docs/en/05_Contributing/02_Release_Process.md index 74f0805cd..a85ab1699 100644 --- a/docs/en/05_Contributing/02_Release_Process.md +++ b/docs/en/05_Contributing/02_Release_Process.md @@ -80,6 +80,25 @@ Here's an example for replacing `Director::isDev()` with a (theoretical) `Env::i 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. +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 ### Reporting an issue diff --git a/tests/control/ControllerTest.php b/tests/control/ControllerTest.php index 1357ae5e0..c92e1a8a3 100644 --- a/tests/control/ControllerTest.php +++ b/tests/control/ControllerTest.php @@ -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'); } diff --git a/tests/core/ConfigTest.php b/tests/core/ConfigTest.php index 306c6ff2b..bbfe3b1a3 100644 --- a/tests/core/ConfigTest.php +++ b/tests/core/ConfigTest.php @@ -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); } } diff --git a/tests/dev/DeprecationTest.php b/tests/dev/DeprecationTest.php index 5a57f36a7..6279d6427 100644 --- a/tests/dev/DeprecationTest.php +++ b/tests/dev/DeprecationTest.php @@ -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() { From 3c20b5806732b76dd817017dffd1b8215fe35edd Mon Sep 17 00:00:00 2001 From: David Craig Date: Wed, 15 Jul 2015 14:58:48 +1200 Subject: [PATCH 12/22] Update tree status after bulk action is applied --- admin/javascript/LeftAndMain.BatchActions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/admin/javascript/LeftAndMain.BatchActions.js b/admin/javascript/LeftAndMain.BatchActions.js index 7eed870a3..ef58e4811 100644 --- a/admin/javascript/LeftAndMain.BatchActions.js +++ b/admin/javascript/LeftAndMain.BatchActions.js @@ -350,8 +350,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 From 79c4f63855e2f38a1177ca73630b93c837059043 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sat, 20 Jun 2015 11:11:08 +0100 Subject: [PATCH 13/22] DOCS Fixing docs (and bad API usage) --- forms/ConfirmedPasswordField.php | 2 +- forms/DatalessField.php | 2 ++ forms/DatetimeField.php | 14 ++++++++--- forms/DropdownField.php | 4 ++++ forms/FileField.php | 4 ++++ forms/FormAction.php | 10 +++++++- forms/HiddenField.php | 2 +- forms/InlineFormAction.php | 31 +++++++++++++++++++++---- forms/MoneyField.php | 9 ++++--- forms/NullableField.php | 7 +++--- forms/PhoneNumberField.php | 4 ++++ forms/ReadonlyField.php | 4 ++++ forms/TreeDropdownField.php | 2 +- forms/gridfield/GridField.php | 8 +++---- security/PermissionCheckboxSetField.php | 16 +++++++++---- 15 files changed, 93 insertions(+), 26 deletions(-) diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index e6a8c6f45..b77107498 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -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'); diff --git a/forms/DatalessField.php b/forms/DatalessField.php index 1f4b38129..97c0f07b9 100644 --- a/forms/DatalessField.php +++ b/forms/DatalessField.php @@ -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); diff --git a/forms/DatetimeField.php b/forms/DatetimeField.php index bccbd0d6e..cff9fdbdb 100644 --- a/forms/DatetimeField.php +++ b/forms/DatetimeField.php @@ -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 . - '
          '; + '
          ' + ); } /** diff --git a/forms/DropdownField.php b/forms/DropdownField.php index c58aeb211..bc9f9ad46 100644 --- a/forms/DropdownField.php +++ b/forms/DropdownField.php @@ -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(); diff --git a/forms/FileField.php b/forms/FileField.php index 9973144df..2728539b9 100644 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -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() diff --git a/forms/FormAction.php b/forms/FormAction.php index abb9d662b..2824e6624 100644 --- a/forms/FormAction.php +++ b/forms/FormAction.php @@ -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); } diff --git a/forms/HiddenField.php b/forms/HiddenField.php index 975eb5d44..052d97d5d 100644 --- a/forms/HiddenField.php +++ b/forms/HiddenField.php @@ -10,7 +10,7 @@ class HiddenField extends FormField { /** * @param array $properties * - * @return string + * @return HTMLText */ public function FieldHolder($properties = array()) { return $this->Field($properties); diff --git a/forms/InlineFormAction.php b/forms/InlineFormAction.php index 3f06ca244..4e8984876 100644 --- a/forms/InlineFormAction.php +++ b/forms/InlineFormAction.php @@ -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 "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 "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() { diff --git a/forms/MoneyField.php b/forms/MoneyField.php index 0bb3b12d0..e7f69b058 100644 --- a/forms/MoneyField.php +++ b/forms/MoneyField.php @@ -41,13 +41,16 @@ class MoneyField extends FormField { } /** - * @return string + * @param array + * @return HTMLText */ public function Field($properties = array()) { - return "
          " . + return DBField::create_field('HTMLText', + "
          " . "
          " . $this->fieldCurrency->SmallFieldHolder() . "
          " . "
          " . $this->fieldAmount->SmallFieldHolder() . "
          " . - "
          "; + "
          " + ); } /** diff --git a/forms/NullableField.php b/forms/NullableField.php index 597495914..0223601af 100644 --- a/forms/NullableField.php +++ b/forms/NullableField.php @@ -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 %s', $this->valueField->Field(), $nullableCheckbox->Field(), $this->getIsNullLabel() - ); + )); } /** diff --git a/forms/PhoneNumberField.php b/forms/PhoneNumberField.php index 0108c307e..2fd0c5dcf 100644 --- a/forms/PhoneNumberField.php +++ b/forms/PhoneNumberField.php @@ -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"); diff --git a/forms/ReadonlyField.php b/forms/ReadonlyField.php index b5a8d595f..856d1cc11 100644 --- a/forms/ReadonlyField.php +++ b/forms/ReadonlyField.php @@ -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) { diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index e8436eced..cf3b7bf2a 100644 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -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'); diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php index 6c2af9a90..c18115514 100644 --- a/forms/gridfield/GridField.php +++ b/forms/gridfield/GridField.php @@ -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); diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index 236b26543..4bb1cafb8 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -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 "
            id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . + return DBField::create_field('HTMLText', + "
              id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . "
            • " . _t( 'Permissions.UserPermissionsIntro', @@ -236,11 +241,14 @@ class PermissionCheckboxSetField extends FormField { ) . "
            • " . $options . - "
            \n"; + "
          \n" + ); } else { - return "
            id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . + return DBField::create_field('HTMLText', + "
              id()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . $options . - "
            \n"; + "
          \n" + ); } } From bf3f159800ec37a7ac5daf892f90fd3f7645c491 Mon Sep 17 00:00:00 2001 From: Peter Thaleikis Date: Wed, 22 Jul 2015 22:35:58 +1200 Subject: [PATCH 14/22] harden addFilter method --- model/DataList.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/model/DataList.php b/model/DataList.php index 5699730e8..9f1a83114 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -372,8 +372,10 @@ 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) { + public function addFilter(array $filterArray) { $list = $this; foreach($filterArray as $field => $value) { From b7480b92a9c734058135a3259a1c4432c6bb474d Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Wed, 22 Jul 2015 14:39:56 +0100 Subject: [PATCH 15/22] FIX: Hide 'Logged Passwords' tab in member CMS fields (fixes #4422) --- security/Member.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/security/Member.php b/security/Member.php index 08b4ab75d..28a368766 100644 --- a/security/Member.php +++ b/security/Member.php @@ -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) { From e26ad64b75c14bca344e8c022f41de6e572547ea Mon Sep 17 00:00:00 2001 From: Peter Thaleikis Date: Thu, 23 Jul 2015 22:28:59 +1200 Subject: [PATCH 16/22] remove the API change --- model/DataList.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/DataList.php b/model/DataList.php index 9f1a83114..813a8453c 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -373,9 +373,9 @@ 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 + * @param array $filterArray */ - public function addFilter(array $filterArray) { + public function addFilter($filterArray) { $list = $this; foreach($filterArray as $field => $value) { From 1831947157f257e4f5bb9fa948e129ea59d9703e Mon Sep 17 00:00:00 2001 From: Simon Erkelens Date: Thu, 23 Jul 2015 20:52:09 +0200 Subject: [PATCH 17/22] Declare the global mapping Fix for #4426 --- core/Constants.php | 1 + 1 file changed, 1 insertion(+) diff --git a/core/Constants.php b/core/Constants.php index ecadf404d..3e958a0ea 100644 --- a/core/Constants.php +++ b/core/Constants.php @@ -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)) { From 5f5ce8a82c2bb1a29f9f8b7011d5cd990c34f128 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 27 Jul 2015 08:26:01 +1200 Subject: [PATCH 18/22] BUG Disable cache to prevent caching of build target --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d4ae8fe28..c0f46cd4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ addons: packages: - tidy -cache: - directories: - - $HOME/.composer/cache - php: - 5.4 From 51722e3d12481056e2ede1083ce5c89b16a7da8d Mon Sep 17 00:00:00 2001 From: Russell Date: Thu, 21 May 2015 15:30:19 +1200 Subject: [PATCH 19/22] DataObject accept arrays or stdClass The constructor of DataObject can take an array or stdClass for $record. However, it is access as an array [here](https://github.com/silverstripe/silverstripe-framework/blob/3.1/model/DataObject.php#L416) and [here](https://github.com/silverstripe/silverstripe-framework/blob/3.1/model/DataObject.php#L431) This pull request ensures $record is an array after validation --- model/DataObject.php | 4 ++++ tests/model/DataObjectTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/model/DataObject.php b/model/DataObject.php index 09ac68d66..d2db29b0c 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -400,6 +400,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) { diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index f4b7507ad..73fbe33ec 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -53,6 +53,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(); From ffbeac6b7d3ed1a6e02a150573ee4b2f9251cf9c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 27 Feb 2015 00:10:32 +0000 Subject: [PATCH 20/22] Ensuring classinfo is case insensitive --- ...ilverStripeServiceConfigurationLocator.php | 32 +- core/ClassInfo.php | 94 +-- model/DataObject.php | 535 +++++++++--------- model/DataQuery.php | 138 ++--- tests/core/ClassInfoTest.php | 74 ++- tests/model/DataListTest.php | 161 +++--- 6 files changed, 554 insertions(+), 480 deletions(-) diff --git a/control/injector/SilverStripeServiceConfigurationLocator.php b/control/injector/SilverStripeServiceConfigurationLocator.php index a0a22bd25..b26b6fcd9 100644 --- a/control/injector/SilverStripeServiceConfigurationLocator.php +++ b/control/injector/SilverStripeServiceConfigurationLocator.php @@ -1,36 +1,36 @@ 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) { - // have we already got for this? + // have we already got for this? $config = $this->configFor($parent); if($config !== null) { // Cache this result @@ -39,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; diff --git a/core/ClassInfo.php b/core/ClassInfo.php index fed9fe7b3..71681f362 100644 --- a/core/ClassInfo.php +++ b/core/ClassInfo.php @@ -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 @@ -35,7 +35,7 @@ class ClassInfo { * @var Array Cache for {@link ancestry()}. */ private static $_cache_ancestry = array(); - + /** * @todo Move this to SS_Database or DB */ @@ -52,17 +52,18 @@ class ClassInfo { return false; } } - + public static function reset_db_cache() { self::$_cache_all_tables = null; self::$_cache_ancestry = array(); } - + /** * Returns the manifest of all classes which are present in the database. * @param string $class Class name to check enum values for ClassName field */ public static function getValidSubClasses($class = 'SiteTree', $includeUnbacked = false) { + $class = self::class_name($class); $classes = DB::getConn()->enumValuesForField($class, 'ClassName'); if (!$includeUnbacked) $classes = array_filter($classes, array('ClassInfo', 'exists')); return $classes; @@ -71,7 +72,7 @@ class ClassInfo { /** * Returns an array of the current class and all its ancestors and children * which have a DB table. - * + * * @param string|object $class * @todo Move this into data object * @return array @@ -79,9 +80,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), @@ -102,7 +101,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"); @@ -121,23 +120,25 @@ class ClassInfo { * Returns a list of classes that inherit from the given class. * The resulting array includes the base class passed * through the $class parameter as the first array value. - * + * * Example usage: * * ClassInfo::subclassesFor('BaseClass'); * array( - * 0 => 'BaseClass', + * 'BaseClass' => 'BaseClass', * 'ChildClass' => 'ChildClass', * 'GrandChildClass' => 'GrandChildClass' * ) * - * + * * @param mixed $class string of the classname or instance of the class * @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); @@ -146,6 +147,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. @@ -155,9 +173,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(); @@ -166,7 +186,7 @@ class ClassInfo { $ancestry[$parent] = $parent; } } while ($parent = get_parent_class($parent)); - self::$_cache_ancestry[$cacheKey] = array_reverse($ancestry); + self::$_cache_ancestry[$cacheKey] = array_reverse($ancestry); } return self::$_cache_ancestry[$cacheKey]; @@ -184,16 +204,16 @@ 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)); } /** * Get all classes contained in a file. * @uses ManifestBuilder - * + * * @todo Doesn't return additional classes that only begin * with the filename, and have additional naming separated through underscores. - * + * * @param string $filePath Path to a PHP file (absolute or relative to webroot) * @return array */ @@ -205,16 +225,16 @@ class ClassInfo { foreach($manifest as $class => $compareFilePath) { if($absFilePath == $compareFilePath) $matchedClasses[] = $class; } - + return $matchedClasses; } - + /** * Returns all classes contained in a certain folder. * * @todo Doesn't return additional classes that only begin * with the filename, and have additional naming separated through underscores. - * + * * @param string $folderPath Relative or absolute folder path * @return array Array of class names */ @@ -233,25 +253,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. * @@ -261,23 +284,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) { diff --git a/model/DataObject.php b/model/DataObject.php index d2db29b0c..526bb5c4e 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -5,16 +5,16 @@ *

          Extensions

          * * See {@link Extension} and {@link DataExtension}. - * + * *

          Permission Control

          - * + * * Object-level access control by {@link Permission}. Permission codes are arbitrary * strings which can be selected on a group-by-group basis. - * + * * * class Article extends DataObject implements PermissionProvider { * static $api_access = true; - * + * * function canView($member = false) { * return Permission::check('ARTICLE_VIEW'); * } @@ -36,13 +36,13 @@ * ); * } * } - * + * * - * Object-level access control by {@link Group} membership: + * Object-level access control by {@link Group} membership: * * class Article extends DataObject { * static $api_access = true; - * + * * function canView($member = false) { * if(!$member) $member = Member::currentUser(); * return $member->inGroup('Subscribers'); @@ -51,18 +51,18 @@ * if(!$member) $member = Member::currentUser(); * return $member->inGroup('Editors'); * } - * + * * // ... * } * - * - * If any public method on this class is prefixed with an underscore, + * + * If any public method on this class is prefixed with an underscore, * the results are cached in memory through {@link cachedCall()}. - * - * + * + * * @todo Add instance specific removeExtension() which undos loadExtraStatics() * and defineMethods() - * + * * @package framework * @subpackage model * @@ -72,21 +72,21 @@ * @property string Created Date and time of DataObject creation. */ class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider { - + /** * Human-readable singular name. * @var string * @config */ private static $singular_name = null; - + /** * Human-readable plural name * @var string * @config */ private static $plural_name = null; - + /** * Allow API access to this object? * @todo Define the options that can be set here @@ -99,18 +99,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @var boolean */ public $destroyed = false; - + /** * The DataModel from this this object comes */ protected $model; - + /** - * Data stored in this objects database record. An array indexed by fieldname. - * + * Data stored in this objects database record. An array indexed by fieldname. + * * Use {@link toMap()} if you want an array representation * of this object, as the $record array might contain lazy loaded field aliases. - * + * * @var array */ protected $record; @@ -119,7 +119,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * An array indexed by fieldname, true if the field has been changed. * Use {@link getChangedFields()} and {@link isChanged()} to inspect * the changed state. - * + * * @var array */ private $changed; @@ -136,13 +136,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @var boolean */ protected $brokenOnDelete = false; - + /** * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite() * @var boolean */ protected $brokenOnWrite = false; - + /** * @config * @var boolean Should dataobjects be validated before they are written? @@ -185,7 +185,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Returns when validation on DataObjects is enabled. - * + * * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead * @return bool */ @@ -193,14 +193,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead'); return Config::inst()->get('DataObject', 'validation_enabled'); } - + /** * Set whether DataObjects should be validated before they are written. - * + * * Caution: Validation can contain safeguards against invalid/malicious data, * and check permission levels (e.g. on {@link Group}). Therefore it is recommended * to only disable validation for very specific use cases. - * + * * @param $enable bool * @see DataObject::validate() * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead @@ -214,7 +214,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @var [string] - class => ClassName field definition cache for self::database_fields */ private static $classname_spec_cache = array(); - + /** * Clear all cached classname specs. It's necessary to clear all cached subclassed names * for any classes if a new class manifest is generated. @@ -260,14 +260,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } /** - * Get all database columns explicitly defined on a class in {@link DataObject::$db} - * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField} - * into the actual database fields, rather than the name of the field which + * Get all database columns explicitly defined on a class in {@link DataObject::$db} + * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField} + * into the actual database fields, rather than the name of the field which * might not equate a database column. - * + * * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited", * see {@link database_fields()}. - * + * * @uses CompositeDBField->compositeDatabaseFields() * * @param string $class @@ -283,14 +283,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) { // Remove the original fieldname, it's not an actual database column unset($fields[$fieldName]); - + // Add all composite columns $compositeFields = singleton($fieldClass)->compositeDatabaseFields(); if($compositeFields) foreach($compositeFields as $compositeName => $spec) { $fields["{$fieldName}{$compositeName}"] = $spec; } } - + // Add has_one relationships $hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED); if($hasOne) foreach(array_keys($hasOne) as $field) { @@ -303,7 +303,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return $output; } - + /** * Returns the field class if the given db field on the class is a composite field. * Will check all applicable ancestor classes and aggregate results. @@ -317,7 +317,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') { @@ -336,14 +336,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public static function composite_fields($class, $aggregated = true) { if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class); - + $compositeFields = DataObject::$_cache_composite_fields[$class]; - + if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') { - $compositeFields = array_merge($compositeFields, + $compositeFields = array_merge($compositeFields, self::composite_fields($parentClass)); } - + return $compositeFields; } @@ -352,7 +352,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ private static function cache_composite_fields($class) { $compositeFields = array(); - + $fields = Config::inst()->get($class, 'db', Config::UNINHERITED); if($fields) foreach($fields as $fieldName => $fieldClass) { if(!is_string($fieldClass)) continue; @@ -360,16 +360,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Strip off any parameters $bPos = strpos($fieldClass, '('); if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos); - + // Test to see if it implements CompositeDBField if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) { $compositeFields[$fieldName] = $fieldClass; } } - + DataObject::$_cache_composite_fields[$class] = $compositeFields; } - + /** * Construct a new DataObject. * @@ -448,7 +448,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // prevent populateDefaults() and setField() from marking overwritten defaults as changed $this->changed = array(); } - + /** * Set the DataModel * @param DataModel $model @@ -481,14 +481,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $className = $this->class; $clone = new $className( $this->toMap(), false, $this->model ); $clone->ID = 0; - + $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite); if($doWrite) { $clone->write(); $this->duplicateManyManyRelations($this, $clone); } $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite); - + return $clone; } @@ -526,7 +526,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Helper function to duplicate relations from one object to another * @param $sourceObject the source object to duplicate from * @param $destinationObject the destination object to populate with the duplicated relations - * @param $name the name of the relation to duplicate (e.g. members) + * @param $name the name of the relation to duplicate (e.g. members) */ private function duplicateRelations($sourceObject, $destinationObject, $name) { $relations = $sourceObject->$name(); @@ -553,7 +553,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if (!ClassInfo::exists($className)) return get_class($this); return $className; } - + /** * Set the ClassName attribute. {@link $class} is also updated. * Warning: This will produce an inconsistent record, as the object @@ -579,7 +579,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * it ensures that the instance of the class is a match for the className of the * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName} * property manually before calling this method, as it will confuse change detection. - * + * * If the new class is different to the original class, defaults are populated again * because this will only occur automatically on instantiation of a DataObject if * there is no record, or the record has no ID. In this case, we do have an ID but @@ -598,7 +598,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity 'RecordClassName' => $originalClass, ) ), false, $this->model); - + if($newClassName != $originalClass) { $newInstance->setClassName($newClassName); $newInstance->populateDefaults(); @@ -667,9 +667,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Returns TRUE if all values (other than "ID") are * considered empty (by weak boolean comparison). * Only checks for fields listed in {@link custom_database_fields()} - * + * * @todo Use DBField->hasValue() - * + * * @return boolean */ public function isEmpty(){ @@ -679,7 +679,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach($map as $k=>$v){ // only look at custom fields if(!array_key_exists($k, $customFields)) continue; - + $dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k); $isEmpty = ($isEmpty && !$dbObj->exists()); } @@ -698,7 +698,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!$name = $this->stat('singular_name')) { $name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class)))); } - + return $name; } @@ -749,14 +749,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $name = $this->plural_name(); return _t($this->class.'.PLURALNAME', $name); } - + /** * Standard implementation of a title/label for a specific * record. Tries to find properties 'Title' or 'Name', * and falls back to the 'ID'. Useful to provide * user-friendly identification of a record, e.g. in errormessages * or UI-selections. - * + * * Overload this method to have a more specialized implementation, * e.g. for an Address record this could be: * @@ -770,7 +770,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public function getTitle() { if($this->hasDatabaseField('Title')) return $this->getField('Title'); if($this->hasDatabaseField('Name')) return $this->getField('Name'); - + return "#{$this->ID}"; } @@ -808,11 +808,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Update a number of fields on this object, given a map of the desired changes. - * + * * The field names can be simple names, or you can use a dot syntax to access $has_one relations. * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim". - * - * update() doesn't write the main object, but if you use the dot syntax, it will write() + * + * update() doesn't write the main object, but if you use the dot syntax, it will write() * the related objects that it alters. * * @param array $data A map of field name to data values to update. @@ -840,8 +840,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } else { user_error( - "DataObject::update(): Can't traverse relationship '$relation'," . - "it has to be a has_one relationship or return a single DataObject", + "DataObject::update(): Can't traverse relationship '$relation'," . + "it has to be a has_one relationship or return a single DataObject", E_USER_NOTICE ); // unset relation object so we don't write properties to the wrong object @@ -865,7 +865,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } return $this; } - + /** * Pass changes as a map, and try to * get automatic casting for these fields. @@ -977,7 +977,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Forces the record to think that all its data has changed. * Doesn't write to the database. Only sets fields as changed * if they are not already marked as changed. - * + * * @return DataObject $this */ public function forceChange() { @@ -986,32 +986,32 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well $fieldNames = array_unique(array_merge( - array_keys($this->record), + array_keys($this->record), array_keys($this->inheritedDatabaseFields()))); - + foreach($fieldNames as $fieldName) { if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1; // Populate the null values in record so that they actually get written if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null; } - + // @todo Find better way to allow versioned to write a new version after forceChange if($this->isChanged('Version')) unset($this->changed['Version']); return $this; } - + /** * Validate the current object. * * By default, there is no validation - objects are always valid! However, you can overload this method in your * DataObject sub-classes to specify custom validation, or use the hook through DataExtension. - * + * * Invalid objects won't be able to be written - a warning will be thrown and no write will occur. onBeforeWrite() * and onAfterWrite() won't get called either. - * + * * It is expected that you call validate() in your own application to test that an object is valid before * attempting a write, and respond appropriately if it isn't. - * + * * @see {@link ValidationResult} * @return ValidationResult */ @@ -1027,12 +1027,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * database. Don't forget to call parent::onBeforeWrite(), though! * * This called after {@link $this->validate()}, so you can be sure that your data is valid. - * + * * @uses DataExtension->onBeforeWrite() */ protected function onBeforeWrite() { $this->brokenOnWrite = false; - + $dummy = null; $this->extend('onBeforeWrite', $dummy); } @@ -1059,11 +1059,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ protected function onBeforeDelete() { $this->brokenOnDelete = false; - + $dummy = null; $this->extend('onBeforeDelete', $dummy); } - + protected function onAfterDelete() { $this->extend('onAfterDelete'); } @@ -1072,22 +1072,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Load the default values in from the self::$defaults array. * Will traverse the defaults of the current class and all its parent classes. * Called by the constructor when creating new records. - * + * * @uses DataExtension->populateDefaults() * @return DataObject $this */ public function populateDefaults() { $classes = array_reverse(ClassInfo::ancestry($this)); - + foreach($classes as $class) { $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED); - + if($defaults && !is_array($defaults)) { user_error("Bad '$this->class' defaults given: " . var_export($defaults, true), E_USER_WARNING); $defaults = null; } - + if($defaults) foreach($defaults as $fieldName => $fieldValue) { // SRM 2007-03-06: Stricter check if(!isset($this->$fieldName) || $this->$fieldName === null) { @@ -1103,7 +1103,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity break; } } - + $this->extend('populateDefaults'); return $this; } @@ -1114,7 +1114,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * - All relevant tables will be updated. * - $this->onBeforeWrite() gets called beforehand. * - Extensions such as Versioned will ammend the database-write to ensure that a version is saved. - * + * * @uses DataExtension->augmentWrite() * * @param boolean $showDebug Show debugging information @@ -1183,7 +1183,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach($this->record as $k => $v) { $this->changed[$k] = 2; } - + $firstWrite = true; } @@ -1207,12 +1207,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } if($hasChanges || $forceWrite || !$this->record['ID']) { - + // New records have their insert into the base data table done first, so that they can pass the // generated primary key on to the rest of the manipulation $baseTable = $ancestry[0]; - - if((!isset($this->record['ID']) || !$this->record['ID']) && isset($ancestry[0])) { + + if((!isset($this->record['ID']) || !$this->record['ID']) && isset($ancestry[0])) { DB::query("INSERT INTO \"{$baseTable}\" (\"Created\") VALUES (" . DB::getConn()->now() . ")"); $this->record['ID'] = DB::getGeneratedID($baseTable); @@ -1226,9 +1226,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(isset($ancestry) && is_array($ancestry)) { foreach($ancestry as $idx => $class) { $classSingleton = singleton($class); - + foreach($this->record as $fieldName => $fieldValue) { - if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] + if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) { $fieldObj = $this->dbObject($fieldName); @@ -1274,13 +1274,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } $this->extend('augmentWrite', $manipulation); - + // New records have their insert into the base data table done first, so that they can pass the // generated ID on to the rest of the manipulation if(isset($isNewRecord) && $isNewRecord && isset($manipulation[$baseTable])) { $manipulation[$baseTable]['command'] = 'update'; } - + DB::manipulate($manipulation); // If there's any relations that couldn't be saved before, save them now (we have an ID here) @@ -1328,13 +1328,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function writeComponents($recursive = false) { if(!$this->components) return $this; - + foreach($this->components as $component) { $component->write(false, false, false, $recursive); } return $this; } - + /** * Delete this data object. * $this->onBeforeDelete() gets called. @@ -1348,7 +1348,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity user_error("$this->class has a broken onBeforeDelete() function." . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR); } - + // Deleting a record without an ID shouldn't do anything if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID"); @@ -1365,7 +1365,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } // Remove this item out of any caches $this->flushCache(); - + $this->onAfterDelete(); $this->OldID = $this->ID; @@ -1417,11 +1417,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(isset($this->components[$componentName])) { return $this->components[$componentName]; } - + if($class = $this->has_one($componentName)) { $joinField = $componentName . 'ID'; $joinID = $this->getField($joinField); - + if($joinID) { $component = $this->model->$class->byID($joinID); } @@ -1432,11 +1432,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } elseif($class = $this->belongs_to($componentName)) { $joinField = $this->getRemoteJoinField($componentName, 'belongs_to'); $joinID = $this->ID; - + if($joinID) { $component = DataObject::get_one($class, "\"$joinField\" = $joinID"); } - + if(!isset($component) || !$component) { $component = $this->model->$class->newObject(); $component->$joinField = $this->ID; @@ -1444,7 +1444,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } else { throw new Exception("DataObject->getComponent(): Could not find component '$componentName'."); } - + $this->components[$componentName] = $component; return $component; } @@ -1479,13 +1479,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!$this->ID) { if(!isset($this->unsavedRelations[$componentName])) { $this->unsavedRelations[$componentName] = - new UnsavedRelationList($this->class, $componentName, $componentClass); + new UnsavedRelationList($this->class, $componentName, $componentClass); } return $this->unsavedRelations[$componentName]; } $joinField = $this->getRemoteJoinField($componentName, 'has_many'); - + $result = HasManyList::create($componentClass, $joinField); if($this->model) $result->setDataModel($this->model); $result = $result->forForeignID($this->ID); @@ -1544,7 +1544,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function getRemoteJoinField($component, $type = 'has_many') { $remoteClass = $this->$type($component, false); - + if(!$remoteClass) { throw new Exception("Unknown $type component '$component' on class '$this->class'"); } @@ -1553,7 +1553,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity "Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'" ); } - + if($fieldPos = strpos($remoteClass, '.')) { return substr($remoteClass, $fieldPos + 1) . 'ID'; } @@ -1563,7 +1563,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $remoteRelations = array(); } $remoteRelations = array_flip($remoteRelations); - + // look for remote has_one joins on this class or any parent classes foreach(array_reverse(ClassInfo::ancestry($this)) as $class) { if(array_key_exists($class, $remoteRelations)) return $remoteRelations[$class] . 'ID'; @@ -1577,7 +1577,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } throw new Exception($message); } - + /** * Returns a many-to-many component, as a ManyManyList. * @param string $componentName Name of the many-many component @@ -1592,11 +1592,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!$this->ID) { if(!isset($this->unsavedRelations[$componentName])) { $this->unsavedRelations[$componentName] = - new UnsavedRelationList($parentClass, $componentName, $componentClass); + new UnsavedRelationList($parentClass, $componentName, $componentClass); } return $this->unsavedRelations[$componentName]; } - + $result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $this->many_many_extraFields($componentName)); if($this->model) $result->setDataModel($this->model); @@ -1604,10 +1604,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // If this is called on a singleton, then we return an 'orphaned relation' that can have the // foreignID set elsewhere. $result = $result->forForeignID($this->ID); - + return $result->where($filter)->sort($sort)->limit($limit); } - + /** * Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and * their classes. @@ -1626,7 +1626,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if($component) { $hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED); - + if(isset($hasOne[$component])) { return $hasOne[$component]; } @@ -1635,7 +1635,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Validate the data foreach($newItems as $k => $v) { if(!is_string($k) || is_numeric($k) || !is_string($v)) { - user_error("$class::\$has_one has a bad entry: " + user_error("$class::\$has_one has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a" . " relationship name, and the map value should be the data class to join to.", E_USER_ERROR); } @@ -1645,7 +1645,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } return isset($items) ? $items : null; } - + /** * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and * their class name will be returned. @@ -1657,7 +1657,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function belongs_to($component = null, $classOnly = true) { $belongsTo = $this->config()->belongs_to; - + if($component) { if($belongsTo && array_key_exists($component, $belongsTo)) { $belongsTo = $belongsTo[$component]; @@ -1665,14 +1665,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return false; } } - + if($belongsTo && $classOnly) { return preg_replace('/(.+)?\..+/', '$1', $belongsTo); } else { return $belongsTo ? $belongsTo : array(); } } - + /** * Return all of the database fields defined in self::$db and all the parent classes. * Doesn't include any fields specified by self::$has_one. Use $this->has_one() to get these fields @@ -1705,7 +1705,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Validate the data foreach($dbItems as $k => $v) { if(!is_string($k) || is_numeric($k) || !is_string($v)) { - user_error("$class::\$db has a bad entry: " + user_error("$class::\$db has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a" . " property name, and the map value should be the property type.", E_USER_ERROR); } @@ -1729,7 +1729,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function has_many($component = null, $classOnly = true) { $hasMany = $this->config()->has_many; - + if($component) { if($hasMany && array_key_exists($component, $hasMany)) { $hasMany = $hasMany[$component]; @@ -1737,7 +1737,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return false; } } - + if($hasMany && $classOnly) { return preg_replace('/(.+)?\..+/', '$1', $hasMany); } else { @@ -1747,10 +1747,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Return the many-to-many extra fields specification. - * + * * If you don't specify a component name, it returns all * extra fields for all components available. - * + * * @param string $component Name of component * @return array */ @@ -1770,13 +1770,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(isset($extraFields[$component])) { return $extraFields[$component]; } - + $manyMany = $SNG_class->stat('many_many'); $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null; if($candidate) { $SNG_candidate = singleton($candidate); $candidateManyMany = $SNG_candidate->stat('belongs_many_many'); - + // Find the relation given the class if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) { if($relatedClass == $class) { @@ -1784,7 +1784,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity break; } } - + if($relationName) { $extraFields = $SNG_candidate->stat('many_many_extraFields'); if(isset($extraFields[$relationName])) { @@ -1792,30 +1792,30 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } } - + $manyMany = $SNG_class->stat('belongs_many_many'); $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null; if($candidate) { $SNG_candidate = singleton($candidate); $candidateManyMany = $SNG_candidate->stat('many_many'); - + // Find the relation given the class if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) { if($relatedClass == $class) { $relationName = $relation; } } - + $extraFields = $SNG_candidate->stat('many_many_extraFields'); if(isset($extraFields[$relationName])) { return $extraFields[$relationName]; } } - - } else { + + } else { // Find all the extra fields for all components $newItems = (array)Config::inst()->get($class, 'many_many_extraFields', Config::UNINHERITED); - + foreach($newItems as $k => $v) { if(!is_array($v)) { user_error( @@ -1826,14 +1826,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ); } } - + $items = isset($items) ? array_merge($newItems, $items) : $newItems; } } return isset($items) ? $items : null; } - + /** * Return information about a many-to-many component. * The return value is an array of (parentclass, childclass). If $component is null, then all many-many @@ -1887,18 +1887,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Validate the data foreach($newItems as $k => $v) { if(!is_string($k) || is_numeric($k) || !is_string($v)) { - user_error("$class::\$many_many has a bad entry: " + user_error("$class::\$many_many has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a" . " relationship name, and the map value should be the data class to join to.", E_USER_ERROR); } } $items = isset($items) ? array_merge($newItems, $items) : $newItems; - + $newItems = (array)Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED); // Validate the data foreach($newItems as $k => $v) { if(!is_string($k) || is_numeric($k) || !is_string($v)) { - user_error("$class::\$belongs_many_many has a bad entry: " + user_error("$class::\$belongs_many_many has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a" . " relationship name, and the map value should be the data class to join to.", E_USER_ERROR); } @@ -1907,20 +1907,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $items = isset($items) ? array_merge($newItems, $items) : $newItems; } } - + return isset($items) ? $items : null; } - + /** * This returns an array (if it exists) describing the database extensions that are required, or false if none - * + * * This is experimental, and is currently only a Postgres-specific enhancement. - * + * * @return array or false */ public function database_extensions($class){ $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED); - + if($extensions) return $extensions; else @@ -1935,12 +1935,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function getDefaultSearchContext() { return new SearchContext( - $this->class, - $this->scaffoldSearchFields(), + $this->class, + $this->scaffoldSearchFields(), $this->defaultSearchFilters() ); } - + /** * Determine which properties on the DataObject are * searchable, and map them to their default {@link FormField} @@ -1950,7 +1950,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * how generic or specific the field type is. * * Used by {@link SearchContext}. - * + * * @param array $_params * 'fieldClasses': Associative array of field names as keys and FormField classes as values * 'restrictFields': Numeric array of a field name whitelist @@ -1967,7 +1967,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $fields = new FieldList(); foreach($this->searchableFields() as $fieldName => $spec) { if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue; - + // If a custom fieldclass is provided as a string, use it if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) { $fieldClass = $params['fieldClasses'][$fieldName]; @@ -1978,17 +1978,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(is_string($spec['field'])) { $fieldClass = $spec['field']; $field = new $fieldClass($fieldName); - + // If it's a FormField object, then just use that object directly. } else if($spec['field'] instanceof FormField) { $field = $spec['field']; - + // Otherwise we have a bug } else { user_error("Bad value for searchable_fields, 'field' value: " . var_export($spec['field'], true), E_USER_WARNING); } - + // Otherwise, use the database field's scaffolder } else { $field = $this->relObject($fieldName)->scaffoldSearchField(); @@ -2010,7 +2010,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}. * * @uses FormScaffolder - * + * * @param array $_params Associative array passing through properties to {@link FormScaffolder}. * @return FieldList */ @@ -2025,27 +2025,27 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ), (array)$_params ); - + $fs = new FormScaffolder($this); $fs->tabbed = $params['tabbed']; $fs->includeRelations = $params['includeRelations']; $fs->restrictFields = $params['restrictFields']; $fs->fieldClasses = $params['fieldClasses']; $fs->ajaxSafe = $params['ajaxSafe']; - + return $fs->getFieldList(); } - + /** * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields * being called on extensions - * + * * @param callable $callback The callback to execute */ protected function beforeUpdateCMSFields($callback) { $this->beforeExtending('updateCMSFields', $callback); } - + /** * Centerpiece of every data administration interface in Silverstripe, * which returns a {@link FieldList} suitable for a {@link Form} object. @@ -2076,16 +2076,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity 'tabbed' => true, 'ajaxSafe' => true )); - + $this->extend('updateCMSFields', $tabbedFields); - + return $tabbedFields; } - + /** * need to be overload by solid dataobject, so that the customised actions of that dataobject, * including that dataobject's extensions customised actions could be added to the EditForm. - * + * * @return an Empty FieldList(); need to be overload by solid subclass */ public function getCMSActions() { @@ -2093,14 +2093,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $this->extend('updateCMSActions', $actions); return $actions; } - + /** * Used for simple frontend forms without relation editing * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()} * by default. To customize, either overload this method in your * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}. - * + * * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API * * @param array $params See {@link scaffoldFormFields()} @@ -2109,7 +2109,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public function getFrontEndFields($params = null) { $untabbedFields = $this->scaffoldFormFields($params); $this->extend('updateFrontEndFields', $untabbedFields); - + return $untabbedFields; } @@ -2148,7 +2148,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // or a valid record has been loaded from the database $value = (isset($this->record[$field])) ? $this->record[$field] : null; if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false); - + $this->record[$field] = $fieldObj; return $this->record[$field]; @@ -2178,7 +2178,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } $dataQuery = new DataQuery($tableClass); - + // Reset query parameter context to that of this DataObject if($params = $this->getSourceQueryParams()) { foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value); @@ -2235,7 +2235,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Return the fields that have changed. - * + * * The change level affects what the functions defines as "changed": * - Level 1 will return strict changes, even !== ones. * - Level 2 is more lenient, it will only return real data changes, for example a change from 0 to null @@ -2254,14 +2254,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function getChangedFields($databaseFieldsOnly = false, $changeLevel = 1) { $changedFields = array(); - + // Update the changed array with references to changed obj-fields foreach($this->record as $k => $v) { if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) { $this->changed[$k] = 2; } } - + if($databaseFieldsOnly) { $databaseFields = $this->inheritedDatabaseFields(); $databaseFields['ID'] = true; @@ -2281,7 +2281,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } } - + if($fields) foreach($fields as $name => $level) { $changedFields[$name] = array( 'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null, @@ -2292,11 +2292,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return $changedFields; } - + /** * Uses {@link getChangedFields()} to determine if fields have been changed * since loading them from the database. - * + * * @param string $fieldName Name of the database field to check, will check for any if not given * @param int $changeLevel See {@link getChangedFields()} * @return boolean @@ -2305,7 +2305,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $changed = $this->getChangedFields(false, $changeLevel); if(!isset($fieldName)) { return !empty($changed); - } + } else { return array_key_exists($fieldName, $changed); } @@ -2340,14 +2340,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(is_object($val) && $this->db($fieldName)) { user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING); } - + // if a field is not existing or has strictly changed if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) { // TODO Add check for php-level defaults which are not set in the db // TODO Add check for hidden input-fields (readonly) which are not set in the db // At the very least, the type has changed $this->changed[$fieldName] = 1; - + if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)) { @@ -2393,8 +2393,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } /** - * Returns true if the given field exists in a database column on any of - * the objects tables and optionally look up a dynamic getter with + * Returns true if the given field exists in a database column on any of + * the objects tables and optionally look up a dynamic getter with * get(). * * @param string $field Name of the field @@ -2421,7 +2421,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return array_key_exists($field, $this->inheritedDatabaseFields()); } - + /** * Returns the field type of the given field, if it belongs to this class, and not a parent. * Note that the field type will not include constructor arguments in round brackets, only the classname. @@ -2437,17 +2437,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if($field == "Created" && get_parent_class($this) == "DataObject") return "SS_Datetime"; // Add fields from Versioned extension - if($field == 'Version' && $this->hasExtension('Versioned')) { + if($field == 'Version' && $this->hasExtension('Versioned')) { return 'Int'; } // get cached fieldmap - $fieldMap = isset(DataObject::$cache_has_own_table_field[$this->class]) - ? DataObject::$cache_has_own_table_field[$this->class] : null; - + $lClass = strtolower($this->class); + $fieldMap = isset(DataObject::$cache_has_own_table_field[$lClass]) + ? DataObject::$cache_has_own_table_field[$lClass] : null; + // if no fieldmap is cached, get all fields if(!$fieldMap) { $fieldMap = Config::inst()->get($this->class, 'db', Config::UNINHERITED); - + // all $db fields on this specific class (no parents) foreach(self::composite_fields($this->class, false) as $fieldname => $fieldtype) { $combined_db = singleton($fieldtype)->compositeDatabaseFields(); @@ -2455,7 +2456,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $fieldMap[$fieldname.$name] = $type; } } - + // all has_one relations on this specific class, // add foreign key $hasOne = Config::inst()->get($this->class, 'has_one', Config::UNINHERITED); @@ -2464,7 +2465,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } // set cached fieldmap - DataObject::$cache_has_own_table_field[$this->class] = $fieldMap; + DataObject::$cache_has_own_table_field[$lClass] = $fieldMap; } // Remove string-based "constructor-arguments" from the DBField definition @@ -2473,7 +2474,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity else return $fieldMap[$field]['type']; } } - + /** * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than * actually looking in the database. @@ -2482,20 +2483,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return bool */ public static function has_own_table($dataClass) { - if(!is_subclass_of($dataClass,'DataObject')) return false; - - if(!isset(DataObject::$cache_has_own_table[$dataClass])) { - if(get_parent_class($dataClass) == 'DataObject') { - DataObject::$cache_has_own_table[$dataClass] = true; + if(!is_subclass_of($dataClass, 'DataObject')) return false; + $lDataClass = strtolower($dataClass); + + if(!isset(DataObject::$cache_has_own_table[$lDataClass])) { + if(get_parent_class($dataClass) == 'DataObject' || Config::inst()->get($dataClass, 'db', Config::UNINHERITED) + || Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED)) { + DataObject::$cache_has_own_table[$lDataClass] = $dataClass; } else { - DataObject::$cache_has_own_table[$dataClass] - = Config::inst()->get($dataClass, 'db', Config::UNINHERITED) - || Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED); + DataObject::$cache_has_own_table[$lDataClass] = false; } } - return DataObject::$cache_has_own_table[$dataClass]; + return (bool)DataObject::$cache_has_own_table[$lDataClass]; } - + /** * Returns true if the member is allowed to do the given action. * See {@link extendedCan()} for a more versatile tri-state permission control. @@ -2569,11 +2570,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Process tri-state responses from permission-alterting extensions. The extensions are * expected to return one of three values: - * + * * - false: Disallow this permission, regardless of what other extensions say * - true: Allow this permission, as long as no other extensions return false * - NULL: Don't affect the outcome - * + * * This method itself returns a tri-state value, and is designed to be used like this: * * @@ -2581,7 +2582,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * if($extended !== null) return $extended; * else return $normalValue; * - * + * * @param String $methodName Method on the same object, e.g. {@link canEdit()} * @param Member|int $member * @return boolean|null @@ -2592,12 +2593,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Remove NULLs $results = array_filter($results, function($v) {return !is_null($v);}); // If there are any non-NULL responses, then return the lowest one of them. - // If any explicitly deny the permission, then we don't get access + // If any explicitly deny the permission, then we don't get access if($results) return min($results); } return null; } - + /** * @param Member $member * @return boolean @@ -2676,7 +2677,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // If we have a CompositeDBField object in $this->record, then return that if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) { return $this->record[$fieldName]; - + // Special case for ID field } else if($fieldName == 'ID') { return new PrimaryKey($fieldName, $this); @@ -2694,7 +2695,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $obj = Object::create_from_string($helper, $fieldName); $obj->setValue($this->$fieldName, $this->record, false); return $obj; - + // Special case for has_one relationships } else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) { $val = $this->$fieldName; @@ -2705,7 +2706,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Traverses to a DBField referenced by relationships between data objects. * - * The path to the related field is specified with dot separated syntax + * The path to the related field is specified with dot separated syntax * (eg: Parent.Child.Child.FieldName). * * @param string $fieldPath @@ -2767,7 +2768,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $component = $component->relation($relation); } elseif($component instanceof DataObject && ($dbObject = $component->dbObject($relation)) - ) { + ) { // Select db object $component = $dbObject; } else { @@ -2775,7 +2776,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } } - + // Bail if the component is null if(!$component) { return null; @@ -2789,7 +2790,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Temporary hack to return an association name, based on class, to get around the mangle * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys. - * + * * @return String */ public function getReverseAssociation($className) { @@ -2805,7 +2806,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $has_one = array_flip($this->has_one()); if (array_key_exists($className, $has_one)) return $has_one[$className]; } - + return false; } @@ -2831,12 +2832,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if($callerClass == 'DataObject') { throw new \InvalidArgumentException('Call ::get() instead of DataObject::get()'); } - + if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) { throw new \InvalidArgumentException('If calling ::get() then you shouldn\'t pass any other' . ' arguments'); } - + $result = DataList::create(get_called_class()); $result->setDataModel(DataModel::inst()); return $result; @@ -2847,7 +2848,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity 'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.' ); } - + $result = DataList::create($callerClass)->where($filter)->sort($sort); if($limit && strpos($limit, ',') !== false) { @@ -2860,7 +2861,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $result->setDataModel(DataModel::inst()); return $result; } - + /** * @deprecated 3.1 Use DataList::create and DataList to do your querying */ @@ -2910,10 +2911,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $cacheKey .= '-' . implode("-", $extra); } $cacheKey = md5($cacheKey); - + // Flush destroyed items out of the cache - if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey]) - && DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject + if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey]) + && DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject && DataObject::$_cache_get_one[$callerClass][$cacheKey]->destroyed) { DataObject::$_cache_get_one[$callerClass][$cacheKey] = false; @@ -2937,12 +2938,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Also clears any cached aggregate data. * * @param boolean $persistent When true will also clear persistent data stored in the Cache system. - * When false will just clear session-local cached data + * When false will just clear session-local cached data * @return DataObject $this */ public function flushCache($persistent = true) { if($persistent) Aggregate::flushCache($this->class); - + if($this->class == 'DataObject') { DataObject::$_cache_get_one = array(); return $this; @@ -2952,9 +2953,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach($classes as $class) { if(isset(DataObject::$_cache_get_one[$class])) unset(DataObject::$_cache_get_one[$class]); } - + $this->extend('flushCache'); - + $this->components = array(); return $this; } @@ -2970,7 +2971,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } DataObject::$_cache_get_one = array(); } - + /** * Reset all global caches associated with DataObject. */ @@ -3020,7 +3021,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * @var Array Parameters used in the query that built this object. - * This can be used by decorators (e.g. lazy loading) to + * This can be used by decorators (e.g. lazy loading) to * run additional queries using the same context. */ protected $sourceQueryParams; @@ -3093,7 +3094,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Check the database schema and update it as necessary. - * + * * @uses DataExtension->augmentDatabase() */ public function requireTable() { @@ -3129,7 +3130,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity "{$this->class}ID" => true, (($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true, ); - + DB::requireTable("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null, $extensions); } @@ -3144,7 +3145,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * database is built, after the database tables have all been created. Overload * this to add default records when the database is built, but make sure you * call parent::requireDefaultRecords(). - * + * * @uses DataExtension->requireDefaultRecords() */ public function requireDefaultRecords() { @@ -3161,11 +3162,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity DB::alteration_message("Added default records to $className table","created"); } } - + // Let any extentions make their own database default data $this->extend('requireDefaultRecords', $dummy); } - + /** * Returns fields bu traversing the class heirachy in a bottom-up direction. * @@ -3179,18 +3180,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public function inheritedDatabaseFields() { $fields = array(); $currentObj = $this->class; - + while($currentObj != 'DataObject') { $fields = array_merge($fields, self::custom_database_fields($currentObj)); $currentObj = get_parent_class($currentObj); } - + return (array) $fields; } /** - * Get the default searchable fields for this object, as defined in the - * $searchable_fields list. If searchable fields are not defined on the + * Get the default searchable fields for this object, as defined in the + * $searchable_fields list. If searchable fields are not defined on the * data object, uses a default selection of summary fields. * * @return array @@ -3199,7 +3200,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // can have mixed format, need to make consistent in most verbose form $fields = $this->stat('searchable_fields'); $labels = $this->fieldLabels(); - + // fallback to summary fields (unless empty array is explicitly specified) if( ! $fields && ! is_array($fields)) { $summaryFields = array_keys($this->summaryFields()); @@ -3223,7 +3224,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } } - + // we need to make sure the format is unified before // augmenting fields, so extensions can apply consistent checks // but also after augmenting fields, because the extension @@ -3263,13 +3264,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } $fields = $rewrite; - + // apply DataExtensions if present $this->extend('updateSearchableFields', $fields); return $fields; } - + /** * Get any user defined searchable fields labels that * exist. Allows overriding of default field names in the form @@ -3282,23 +3283,23 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * would generally only be set in the case of more complex relationships * between data object being required in the search interface. * - * Generates labels based on name of the field itself, if no static property + * Generates labels based on name of the field itself, if no static property * {@link self::field_labels} exists. * * @uses $field_labels * @uses FormField::name_to_label() * * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields - * + * * @return array|string Array of all element labels if no argument given, otherwise the label of the field */ public function fieldLabels($includerelations = true) { $cacheKey = $this->class . '_' . $includerelations; - + if(!isset(self::$_cache_field_labels[$cacheKey])) { $customLabels = $this->stat('field_labels'); $autoLabels = array(); - + // get all translated static properties as defined in i18nCollectStatics() $ancestry = ClassInfo::ancestry($this->class); $ancestry = array_reverse($ancestry); @@ -3321,20 +3322,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } $labels = array_merge((array)$autoLabels, (array)$customLabels); - $this->extend('updateFieldLabels', $labels); + $this->extend('updateFieldLabels', $labels); self::$_cache_field_labels[$cacheKey] = $labels; } - + return self::$_cache_field_labels[$cacheKey]; } - + /** * Get a human-readable label for a single field, * see {@link fieldLabels()} for more details. - * + * * @uses fieldLabels() * @uses FormField::name_to_label() - * + * * @param string $name Name of the field * @return string Label of the field */ @@ -3368,7 +3369,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name'; } $this->extend("updateSummaryFields", $fields); - + // Final fail-over, just list ID field if(!$fields) $fields['ID'] = 'ID'; @@ -3400,7 +3401,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach($this->searchableFields() as $name => $spec) { $filterClass = $spec['filter']; - + if($spec['filter'] instanceof SearchFilter) { $filters[$name] = $spec['filter']; } else { @@ -3427,8 +3428,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /* * @ignore */ - private static $subclass_access = true; - + private static $subclass_access = true; + /** * Temporarily disable subclass access in data object qeur */ @@ -3438,7 +3439,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public static function enable_subclass_access() { self::$subclass_access = true; } - + //-------------------------------------------------------------------------------------------// /** @@ -3462,13 +3463,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity "Created" => "SS_Datetime", "Title" => 'Text', ); - + /** * Specify custom options for a CREATE TABLE call. * Can be used to specify a custom storage engine for specific database table. * All options have to be keyed for a specific database implementation, * identified by their class name (extending from {@link SS_Database}). - * + * * * array( * 'MySQLDatabase' => 'ENGINE=MyISAM' @@ -3477,7 +3478,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * * Caution: This API is experimental, and might not be * included in the next major release. Please use with care. - * + * * @var array * @config */ @@ -3489,7 +3490,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * If a field is in this array, then create a database index * on that field. This is a map from fieldname to index type. * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation. - * + * * @var array * @config */ @@ -3499,11 +3500,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Inserts standard column-values when a DataObject * is instanciated. Does not insert default records {@see $default_records}. * This is a map from fieldname to default value. - * + * * - If you would like to change a default value in a sub-class, just specify it. * - If you would like to disable the default value given by a parent class, set the default value to 0,'', * or false in your subclass. Setting it to null won't work. - * + * * @var array * @config */ @@ -3536,7 +3537,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @config */ private static $has_one = null; - + /** * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}. * @@ -3550,7 +3551,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @config */ private static $belongs_to; - + /** * This defines a one-to-many relationship. It is a map of component name to the remote data class. * @@ -3575,7 +3576,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Extra fields to include on the connecting many-many table. * This is a map from field name to field type. - * + * * Example code: * * public static $many_many_extraFields = array( @@ -3584,7 +3585,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * ) * ); * - * + * * @var array * @config */ @@ -3616,7 +3617,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * "Name" => "PartialMatchFilter" * ); * - * + * * Overriding the default form fields, with a custom defined field. * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}. * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}. @@ -3632,7 +3633,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * * static $searchable_fields = array( * "Organisation.ZipCode" => array( - * "field" => "TextField", + * "field" => "TextField", * "filter" => "PartialMatchFilter", * "title" => 'Organisation ZIP' * ) @@ -3655,44 +3656,44 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @config */ private static $summary_fields = null; - + /** * Provides a list of allowed methods that can be called via RESTful api. */ public static $allowed_actions = null; - + /** * Collect all static properties on the object * which contain natural language, and need to be translated. * The full entity name is composed from the class name and a custom identifier. - * + * * @return array A numerical array which contains one or more entities in array-form. * Each numeric entity array contains the "arguments" for a _t() call as array values: * $entity, $string, $priority, $context. */ public function provideI18nEntities() { $entities = array(); - + $entities["{$this->class}.SINGULARNAME"] = array( $this->singular_name(), - + 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' ); $entities["{$this->class}.PLURALNAME"] = array( $this->plural_name(), - + 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the' . ' interface' ); - + return $entities; } - + /** * Returns true if the given method/parameter has a value * (Uses the DBField::hasValue if the parameter is a database field) - * + * * @param string $field The field name * @param array $arguments * @param bool $cache diff --git a/model/DataQuery.php b/model/DataQuery.php index 21b259dbd..625848e3f 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -12,34 +12,34 @@ * @package framework */ class DataQuery { - + /** * @var string */ protected $dataClass; - + /** * @var SQLQuery */ protected $query; - + /** * @var array */ protected $collidingFields = array(); private $queriedColumns = null; - + /** * @var Boolean */ private $queryFinalised = false; - + // TODO: replace subclass_access with this protected $querySubclasses = true; // TODO: replace restrictclasses with this protected $filterByClassName = true; - + /** * Create a new DataQuery. * @@ -49,14 +49,14 @@ class DataQuery { $this->dataClass = $dataClass; $this->initialiseQuery(); } - + /** * Clone this object */ public function __clone() { $this->query = clone $this->query; } - + /** * Return the {@link DataObject} class that is being queried. */ @@ -71,8 +71,8 @@ class DataQuery { public function query() { return $this->getFinalisedQuery(); } - - + + /** * Remove a filter from the query */ @@ -96,7 +96,7 @@ class DataQuery { return $this; } - + /** * Set up the simplest initial query */ @@ -104,7 +104,7 @@ class DataQuery { // Get the tables to join to. // Don't get any subclass tables - let lazy loading do that. $tableClasses = ClassInfo::ancestry($this->dataClass, true); - + // Error checking if(!$tableClasses) { if(!SS_ClassLoader::instance()->hasManifest()) { @@ -122,7 +122,7 @@ class DataQuery { // Build our intial query $this->query = new SQLQuery(array()); $this->query->setDistinct(true); - + if($sort = singleton($this->dataClass)->stat('default_sort')) { $this->sort($sort); } @@ -169,7 +169,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 @@ -183,10 +183,10 @@ class DataQuery { $tableFields = DataObject::database_fields($tableClass); $selectColumns = array_intersect($queriedColumns, array_keys($tableFields)); } - + // If this is a subclass without any explicitly requested columns, omit this from the query if(!in_array($tableClass, $ancestorTables) && empty($selectColumns)) continue; - + // Select necessary columns (unless an explicitly empty array) if($selectColumns !== array()) { $this->selectColumnsFromTable($query, $tableClass, $selectColumns); @@ -197,7 +197,7 @@ class DataQuery { $query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"", $tableClass, 10); } } - + // Resolve colliding fields if($this->collidingFields) { foreach($this->collidingFields as $k => $collisions) { @@ -246,7 +246,7 @@ class DataQuery { /** * Ensure that if a query has an order by clause, those columns are present in the select. - * + * * @param SQLQuery $query * @return null */ @@ -259,7 +259,7 @@ class DataQuery { $i = 0; foreach($orderby as $k => $dir) { $newOrderby[$k] = $dir; - + // don't touch functions in the ORDER BY or public function calls // selected as fields if(strpos($k, '(') !== false) continue; @@ -275,26 +275,26 @@ class DataQuery { continue; } - + if(count($parts) == 1) { $databaseFields = DataObject::database_fields($baseClass); - - // database_fields() doesn't return ID, so we need to + + // database_fields() doesn't return ID, so we need to // manually add it here $databaseFields['ID'] = true; - + if(isset($databaseFields[$parts[0]])) { $qualCol = "\"$baseClass\".\"{$parts[0]}\""; } else { $qualCol = "\"$parts[0]\""; } - + // remove original sort unset($newOrderby[$k]); // add new columns sort $newOrderby[$qualCol] = $dir; - + // To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect() // format internally; then this check can be part of selectField() $selects = $query->getSelect(); @@ -306,7 +306,7 @@ class DataQuery { if(!in_array($qualCol, $query->getSelect())) { unset($newOrderby[$k]); - + $newOrderby["\"_SortColumn$i\""] = $dir; $query->selectField($qualCol, "_SortColumn$i"); @@ -345,7 +345,7 @@ class DataQuery { /** * Return the maximum value of the given field in this DataList - * + * * @param String $field Unquoted database column name (will be escaped automatically) */ public function max($field) { @@ -354,16 +354,16 @@ class DataQuery { /** * Return the minimum value of the given field in this DataList - * + * * @param String $field Unquoted database column name (will be escaped automatically) */ public function min($field) { return $this->aggregate(sprintf('MIN("%s")', Convert::raw2sql($field))); } - + /** * Return the average value of the given field in this DataList - * + * * @param String $field Unquoted database column name (will be escaped automatically) */ public function avg($field) { @@ -372,13 +372,13 @@ class DataQuery { /** * Return the sum of the values of the given field in this DataList - * + * * @param String $field Unquoted database column name (will be escaped automatically) */ public function sum($field) { return $this->aggregate(sprintf('SUM("%s")', Convert::raw2sql($field))); } - + /** * Runs a raw aggregate expression. Please handle escaping yourself */ @@ -415,7 +415,7 @@ class DataQuery { if($expressionForField = $query->expressionForField($k)) { if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($expressionForField); $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; - + } else { $query->selectField("\"$tableClass\".\"$k\"", $k); } @@ -428,20 +428,20 @@ class DataQuery { } } } - + /** * Append a GROUP BY clause to this query. - * + * * @param String $groupby Escaped SQL statement */ public function groupby($groupby) { $this->query->addGroupBy($groupby); return $this; } - + /** * Append a HAVING clause to this query. - * + * * @param String $having Escaped SQL statement */ public function having($having) { @@ -494,7 +494,7 @@ class DataQuery { /** * Append a WHERE with OR. - * + * * @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'")); * @see where() * @@ -507,7 +507,7 @@ class DataQuery { } return $this; } - + /** * Set the ORDER BY clause of this query * @@ -524,10 +524,10 @@ class DataQuery { } else { $this->query->addOrderBy($sort, $direction); } - + return $this; } - + /** * Reverse order by clause * @@ -537,10 +537,10 @@ class DataQuery { $this->query->reverseOrderBy(); return $this; } - + /** * Set the limit of this query. - * + * * @param int $limit * @param int $offset */ @@ -562,7 +562,7 @@ class DataQuery { /** * Add an INNER JOIN clause to this query. - * + * * @param String $table The unquoted table name to join to. * @param String $onClause The filter for the join (escaped SQL statement) * @param String $alias An optional alias name (unquoted) @@ -576,7 +576,7 @@ class DataQuery { /** * Add a LEFT JOIN clause to this query. - * + * * @param String $table The unquoted table to join to. * @param String $onClause The filter for the join (escaped SQL statement). * @param String $alias An optional alias name (unquoted) @@ -592,18 +592,18 @@ class DataQuery { * Traverse the relationship fields, and add the table * mappings to the query object state. This has to be called * in any overloaded {@link SearchFilter->apply()} methods manually. - * + * * @param String|array $relation The array/dot-syntax relation to follow * @return The model class of the related item */ public function applyRelation($relation) { // NO-OP if(!$relation) return $this->dataClass; - + if(is_string($relation)) $relation = explode(".", $relation); $modelClass = $this->dataClass; - + foreach($relation as $rel) { $model = singleton($modelClass); if ($component = $model->has_one($rel)) { @@ -612,7 +612,7 @@ class DataQuery { $realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID"); $this->query->addLeftJoin($component, "\"$component\".\"ID\" = \"{$realModelClass}\".\"{$foreignKey}ID\""); - + /** * add join clause to the component's ancestry classes so that the search filter could search on * its ancestor fields. @@ -667,15 +667,15 @@ class DataQuery { } } - + return $modelClass; } - + /** * Removes the result of query from this query. - * + * * @param DataQuery $subtractQuery - * @param string $field + * @param string $field */ public function subtract(DataQuery $subtractQuery, $field='ID') { $fieldExpression = $subtractQuery->expressionForField($field); @@ -690,15 +690,15 @@ class DataQuery { /** * Select the given fields from the given table. - * + * * @param String $table Unquoted table name (will be escaped automatically) * @param Array $fields Database column names (will be escaped automatically) */ public function selectFromTable($table, $fields) { $table = Convert::raw2sql($table); - $fieldExpressions = array_map(create_function('$item', + $fieldExpressions = array_map(create_function('$item', "return '\"$table\".\"' . Convert::raw2sql(\$item) . '\"';"), $fields); - + $this->query->setSelect($fieldExpressions); return $this; @@ -706,7 +706,7 @@ class DataQuery { /** * Query the given field column from the database and return as an array. - * + * * @param string $field See {@link expressionForField()}. * @return array List of column values for the specified column */ @@ -720,31 +720,31 @@ class DataQuery { return $query->execute()->column($field); } - + /** * @param String $field Select statement identifier, either the unquoted column name, * the full composite SQL statement, or the alias set through {@link SQLQuery->selectField()}. * @return String The expression used to query this field via this DataQuery */ protected function expressionForField($field) { - + // Prepare query object for selecting this field $query = $this->getFinalisedQuery(array($field)); - + // Allow query to define the expression for this field $expression = $query->expressionForField($field); if(!empty($expression)) return $expression; - + // Special case for ID, if not provided if($field === 'ID') { $baseClass = ClassInfo::baseDataClass($this->dataClass); - return "\"$baseClass\".\"ID\""; + return "\"$baseClass\".\"ID\""; } } /** * Select the given field expressions. - * + * * @param $fieldExpression String The field to select (escaped SQL statement) * @param $alias String The alias of that field (escaped SQL statement) */ @@ -759,7 +759,7 @@ class DataQuery { * @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery. */ private $queryParams; - + /** * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'. @@ -767,7 +767,7 @@ class DataQuery { public function setQueryParam($key, $value) { $this->queryParams[$key] = $value; } - + /** * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. */ @@ -788,7 +788,7 @@ class DataQuery { /** * Represents a subgroup inside a WHERE clause in a {@link DataQuery} * - * Stores the clauses for the subgroup inside a specific {@link SQLQuery} + * Stores the clauses for the subgroup inside a specific {@link SQLQuery} * object. * * All non-where methods call their DataQuery versions, which uses the base @@ -833,7 +833,7 @@ class DataQuery_SubGroup extends DataQuery { /** * Set a WHERE with OR. - * + * * @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'")); * @see where() * @@ -855,10 +855,10 @@ class DataQuery_SubGroup extends DataQuery { } $sql = DB::getConn()->sqlWhereToString( - $this->whereQuery->getWhere(), + $this->whereQuery->getWhere(), $this->whereQuery->getConnective() ); - + $sql = preg_replace('[^\s*WHERE\s*]', '', $sql); return $sql; diff --git a/tests/core/ClassInfoTest.php b/tests/core/ClassInfoTest.php index 844b7c84d..cc6952b7d 100644 --- a/tests/core/ClassInfoTest.php +++ b/tests/core/ClassInfoTest.php @@ -6,10 +6,18 @@ */ class ClassInfoTest extends SapphireTest { + 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() { @@ -22,23 +30,33 @@ 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() { //$baseFolder = Director::baseFolder() . '/' . FRAMEWORK_DIR . '/tests/_ClassInfoTest'; //$manifestInfo = ManifestBuilder::get_manifest_info($baseFolder); - + $classes = ClassInfo::classes_for_folder(FRAMEWORK_DIR . '/tests'); $this->assertContains( 'classinfotest', $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' + ); } /** @@ -46,8 +64,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'); @@ -67,6 +88,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' @@ -89,16 +117,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() { @@ -106,19 +140,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') ); @@ -147,7 +189,7 @@ class ClassInfoTest extends SapphireTest { */ class ClassInfoTest_BaseClass extends DataObject { - + } /** @@ -156,7 +198,7 @@ class ClassInfoTest_BaseClass extends DataObject { */ class ClassInfoTest_ChildClass extends ClassInfoTest_BaseClass { - + } /** @@ -165,7 +207,7 @@ class ClassInfoTest_ChildClass extends ClassInfoTest_BaseClass { */ class ClassInfoTest_GrandChildClass extends ClassInfoTest_ChildClass { - + } /** diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 5ff707a03..faf9b353b 100755 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -5,7 +5,7 @@ * @subpackage tests */ class DataListTest extends SapphireTest { - + // Borrow the model from DataObjectTest protected static $fixture_file = 'DataObjectTest.yml'; @@ -69,32 +69,32 @@ class DataListTest extends SapphireTest { $newList = $fullList->subtract($subtractList); $this->assertEquals(2, $newList->Count(), 'List should only contain two objects after subtraction'); } - + public function testSubtractBadDataclassThrowsException(){ $this->setExpectedException('InvalidArgumentException'); $teamsComments = DataObjectTest_TeamComment::get(); $teams = DataObjectTest_Team::get(); $teamsComments->subtract($teams); } - + public function testListCreationSortAndLimit() { // By default, a DataList will contain all items of that class $list = DataObjectTest_TeamComment::get()->sort('ID'); - + // We can iterate on the DataList $names = array(); foreach($list as $item) { $names[] = $item->Name; } $this->assertEquals(array('Joe', 'Bob', 'Phil'), $names); - + // If we don't want to iterate, we can extract a single column from the list with column() $this->assertEquals(array('Joe', 'Bob', 'Phil'), $list->column('Name')); - + // We can sort a list $list = $list->sort('Name'); $this->assertEquals(array('Bob', 'Joe', 'Phil'), $list->column('Name')); - + // We can also restrict the output to a range $this->assertEquals(array('Joe', 'Phil'), $list->limit(2, 1)->column('Name')); } @@ -137,12 +137,17 @@ class DataListTest extends SapphireTest { $list = DataObjectTest_TeamComment::get(); $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)); } - + public function testSql() { $db = DB::getConn(); $list = DataObjectTest_TeamComment::get(); @@ -154,7 +159,7 @@ class DataListTest extends SapphireTest { . ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment"'; $this->assertEquals($expected, $list->sql()); } - + public function testInnerJoin() { $db = DB::getConn(); @@ -175,7 +180,7 @@ class DataListTest extends SapphireTest { $this->assertEquals($expected, $list->sql()); } - + public function testLeftJoin() { $db = DB::getConn(); @@ -199,7 +204,7 @@ class DataListTest extends SapphireTest { // Test with namespaces (with non-sensical join, but good enough for testing) $list = DataObjectTest_TeamComment::get(); $list = $list->leftJoin( - 'DataObjectTest\NamespacedClass', + 'DataObjectTest\NamespacedClass', '"DataObjectTest\NamespacedClass"."ID" = "DataObjectTest_TeamComment"."ID"' ); @@ -209,9 +214,9 @@ class DataListTest extends SapphireTest { . '"DataObjectTest_TeamComment"."Name", ' . '"DataObjectTest_TeamComment"."Comment", ' . '"DataObjectTest_TeamComment"."TeamID", ' - . '"DataObjectTest_TeamComment"."ID", ' + . '"DataObjectTest_TeamComment"."ID", ' . 'CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL ' - . 'THEN "DataObjectTest_TeamComment"."ClassName" ' + . 'THEN "DataObjectTest_TeamComment"."ClassName" ' . 'ELSE ' . $db->prepStringForDB('DataObjectTest_TeamComment') . ' END AS "RecordClassName" ' . 'FROM "DataObjectTest_TeamComment" ' . 'LEFT JOIN "DataObjectTest\NamespacedClass" ON ' @@ -219,7 +224,7 @@ class DataListTest extends SapphireTest { $this->assertEquals($expected, $list->sql(), 'Retains backslashes in namespaced classes'); } - + public function testToNestedArray() { $list = DataObjectTest_TeamComment::get()->sort('ID'); $nestedArray = $list->toNestedArray(); @@ -251,7 +256,7 @@ class DataListTest extends SapphireTest { $this->assertEquals($expected[1]['Comment'], $nestedArray[1]['Comment']); $this->assertEquals($expected[2]['TeamID'], $nestedArray[2]['TeamID']); } - + public function testMap() { $map = DataObjectTest_TeamComment::get()->map()->toArray(); $expected = array( @@ -259,7 +264,7 @@ class DataListTest extends SapphireTest { $this->idFromFixture('DataObjectTest_TeamComment', 'comment2') => 'Bob', $this->idFromFixture('DataObjectTest_TeamComment', 'comment3') => 'Phil' ); - + $this->assertEquals($expected, $map); $otherMap = DataObjectTest_TeamComment::get()->map('Name', 'TeamID')->toArray(); $otherExpected = array( @@ -267,22 +272,22 @@ class DataListTest extends SapphireTest { 'Bob' => $this->objFromFixture('DataObjectTest_TeamComment', 'comment2')->TeamID, 'Phil' => $this->objFromFixture('DataObjectTest_TeamComment', 'comment3')->TeamID ); - + $this->assertEquals($otherExpected, $otherMap); } - + public function testEach() { $list = DataObjectTest_TeamComment::get(); - + $count = 0; $test = $this; - + $list->each(function($item) use (&$count, $test) { $count++; - + $test->assertTrue(is_a($item, "DataObjectTest_TeamComment")); }); - + $this->assertEquals($list->Count(), $count); } @@ -290,16 +295,16 @@ class DataListTest extends SapphireTest { // We can use raw SQL queries with where. This is only recommended for advanced uses; // if you can, you should use filter(). $list = DataObjectTest_TeamComment::get(); - + // where() returns a new DataList, like all the other modifiers, so it can be chained. $list2 = $list->where('"Name" = \'Joe\''); $this->assertEquals(array('This is a team comment by Joe'), $list2->column('Comment')); - + // The where() clauses are chained together with AND $list3 = $list2->where('"Name" = \'Bob\''); $this->assertEquals(array(), $list3->column('Comment')); } - + /** * Test DataList->byID() */ @@ -307,24 +312,24 @@ class DataListTest extends SapphireTest { // We can get a single item by ID. $id = $this->idFromFixture('DataObjectTest_Team','team2'); $team = DataObjectTest_Team::get()->byID($id); - + // byID() returns a DataObject, rather than a DataList $this->assertInstanceOf('DataObjectTest_Team', $team); $this->assertEquals('Team 2', $team->Title); } - + /** * Test DataList->removeByID() */ public function testRemoveByID() { $list = DataObjectTest_Team::get(); $id = $this->idFromFixture('DataObjectTest_Team','team2'); - + $this->assertNotNull($list->byID($id)); $list->removeByID($id); $this->assertNull($list->byID($id)); } - + /** * Test DataList->canSortBy() */ @@ -333,28 +338,28 @@ class DataListTest extends SapphireTest { $team = DataObjectTest_Team::get(); $this->assertTrue($team->canSortBy("Title")); $this->assertFalse($team->canSortBy("SomethingElse")); - + // Subclasses $subteam = DataObjectTest_SubTeam::get(); $this->assertTrue($subteam->canSortBy("Title")); $this->assertTrue($subteam->canSortBy("SubclassDatabaseField")); } - + public function testDataListArrayAccess() { $list = DataObjectTest_Team::get()->sort('Title'); - + // We can use array access to refer to single items in the DataList, as if it were an array $this->assertEquals("Subteam 1", $list[0]->Title); $this->assertEquals("Subteam 3", $list[2]->Title); $this->assertEquals("Team 2", $list[4]->Title); } - + public function testFind() { $list = DataObjectTest_Team::get(); $record = $list->find('Title', 'Team 1'); $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $record->ID); } - + public function testFindById() { $list = DataObjectTest_Team::get(); $record = $list->find('ID', $this->idFromFixture('DataObjectTest_Team', 'team1')); @@ -363,70 +368,70 @@ class DataListTest extends SapphireTest { $record = $list->find('ID', $this->idFromFixture('DataObjectTest_Team', 'team2')); $this->assertEquals('Team 2', $record->Title); } - + public function testSimpleSort() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name'); $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testSimpleSortOneArgumentASC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name ASC'); $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testSimpleSortOneArgumentDESC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name DESC'); $this->assertEquals('Phil', $list->first()->Name, 'Last comment should be from Phil'); $this->assertEquals('Bob', $list->last()->Name, 'First comment should be from Bob'); } - + public function testSortOneArgumentMultipleColumns() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('TeamID ASC, Name DESC'); $this->assertEquals('Joe', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testSimpleSortASC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name', 'asc'); $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testSimpleSortDESC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name', 'desc'); $this->assertEquals('Phil', $list->first()->Name, 'Last comment should be from Phil'); $this->assertEquals('Bob', $list->last()->Name, 'First comment should be from Bob'); } - + public function testSortWithArraySyntaxSortASC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort(array('Name'=>'asc')); $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testSortWithArraySyntaxSortDESC() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort(array('Name'=>'desc')); $this->assertEquals('Phil', $list->first()->Name, 'Last comment should be from Phil'); $this->assertEquals('Bob', $list->last()->Name, 'First comment should be from Bob'); } - + public function testSortWithMultipleArraySyntaxSort() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort(array('TeamID'=>'asc','Name'=>'desc')); $this->assertEquals('Joe', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + /** * $list->filter('Name', 'bob'); // only bob in the list */ @@ -437,7 +442,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Team 2', $list->first()->Title, 'List should only contain Team 2'); $this->assertEquals('Team 2', $list->last()->Title, 'Last should only contain Team 2'); } - + public function testSimpleFilterEndsWith() { $list = DataObjectTest_TeamComment::get(); $list = $list->filter('Name:EndsWith', 'b'); @@ -518,7 +523,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testMultipleFilterWithNoMatch() { $list = DataObjectTest_TeamComment::get(); $list = $list->filter(array('Name'=>'Bob', 'Comment'=>'Phil is a unique guy, and comments on team2')); @@ -535,13 +540,13 @@ class DataListTest extends SapphireTest { $this->assertEquals(1, $list->count()); $this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob'); } - + public function testFilterMultipleWithTwoMatches() { $list = DataObjectTest_TeamComment::get(); $list = $list->filter(array('TeamID'=>$this->idFromFixture('DataObjectTest_Team', 'team1'))); $this->assertEquals(2, $list->count()); } - + public function testFilterMultipleWithArrayFilter() { $list = DataObjectTest_TeamComment::get(); $list = $list->filter(array('Name'=>array('Bob','Phil'))); @@ -559,7 +564,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Bob', $list->first()->Name); $this->assertEquals('Joe', $list->last()->Name); } - + /** * $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); */ @@ -587,7 +592,7 @@ class DataListTest extends SapphireTest { $list = DataObjectTest_TeamComment::get(); $list = $list->filterAny('Name', 'Bob'); $this->assertEquals(1, $list->count()); - } + } public function testFilterAnyMultipleArray() { $list = DataObjectTest_TeamComment::get(); @@ -608,13 +613,13 @@ class DataListTest extends SapphireTest { $list = $list->sort('Name'); $this->assertEquals(2, $list->count()); $this->assertEquals( - 'Bob', - $list->offsetGet(0)->Name, + 'Bob', + $list->offsetGet(0)->Name, 'Results should include comments from Bob, matched by comment and team' ); $this->assertEquals( - 'Joe', - $list->offsetGet(1)->Name, + 'Joe', + $list->offsetGet(1)->Name, 'Results should include comments by Joe, matched by name and team (not by comment)' ); @@ -630,12 +635,12 @@ class DataListTest extends SapphireTest { $list = $list->filter(array('Name' => 'Bob')); $this->assertEquals(1, $list->count()); $this->assertEquals( - 'Bob', - $list->offsetGet(0)->Name, + 'Bob', + $list->offsetGet(0)->Name, 'Results should include comments from Bob, matched by name and team' ); } - + public function testFilterAnyMultipleWithArrayFilter() { $list = DataObjectTest_TeamComment::get(); $list = $list->filterAny(array('Name'=>array('Bob','Phil'))); @@ -643,7 +648,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } - + public function testFilterAnyArrayInArray() { $list = DataObjectTest_TeamComment::get(); $list = $list->filterAny(array( @@ -652,18 +657,18 @@ class DataListTest extends SapphireTest { ->sort('Name'); $this->assertEquals(3, $list->count()); $this->assertEquals( - 'Bob', - $list->offsetGet(0)->Name, + 'Bob', + $list->offsetGet(0)->Name, 'Results should include comments from Bob, matched by name and team' ); $this->assertEquals( - 'Joe', - $list->offsetGet(1)->Name, + 'Joe', + $list->offsetGet(1)->Name, 'Results should include comments by Joe, matched by team (not by name)' ); $this->assertEquals( - 'Phil', - $list->offsetGet(2)->Name, + 'Phil', + $list->offsetGet(2)->Name, 'Results should include comments from Phil, matched by name (even if he\'s not in Team1)' ); } @@ -750,7 +755,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Joe', $list->first()->Name, 'First comment should be from Joe'); $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } -// +// /** * $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list */ @@ -769,7 +774,7 @@ class DataListTest extends SapphireTest { $list = $list->exclude(array('Name'=>'Bob', 'Comment'=>'Does not match any comments')); $this->assertEquals(3, $list->count()); } - + /** * $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 */ @@ -786,7 +791,7 @@ class DataListTest extends SapphireTest { $list = DataObjectTest_TeamComment::get(); $list = $list->filter('Comment', 'Phil is a unique guy, and comments on team2'); $list = $list->exclude('Name', 'Bob'); - + $this->assertContains( 'WHERE ("DataObjectTest_TeamComment"."Comment" = ' . '\'Phil is a unique guy, and comments on team2\') ' @@ -799,7 +804,7 @@ class DataListTest extends SapphireTest { $list = $list->exclude('Name:LessThan', 'Bob'); $this->assertContains('WHERE (("DataObjectTest_TeamComment"."Name" >= \'Bob\'))', $list->sql()); } - + /** * $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 */ @@ -814,7 +819,7 @@ class DataListTest extends SapphireTest { $this->assertEquals('Joe', $list->first()->Name, 'First comment should be from Phil'); $this->assertEquals('Phil', $list->last()->Name, 'First comment should be from Phil'); } - + /** * $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // negative version */ @@ -823,7 +828,7 @@ class DataListTest extends SapphireTest { $list = $list->exclude(array('Name'=>'Bob', 'TeamID'=>array(3))); $this->assertEquals(3, $list->count()); } - + /** * $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); //negative version */ @@ -834,9 +839,9 @@ class DataListTest extends SapphireTest { 'Comment' => 'Phil is a unique guy, and comments on team2')); $this->assertEquals(3, $list->count()); } - + /** - * $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); + * $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); */ public function testMultipleExcludeWithTwoArray() { $list = DataObjectTest_TeamComment::get(); @@ -847,9 +852,9 @@ class DataListTest extends SapphireTest { $this->assertEquals(1, $list->count()); $this->assertEquals('Phil', $list->last()->Name, 'Only comment should be from Phil'); } - + /** - * $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); + * $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); */ public function testMultipleExcludeWithTwoArrayOneTeam() { $list = DataObjectTest_TeamComment::get(); @@ -863,7 +868,7 @@ class DataListTest extends SapphireTest { } /** - * + * */ public function testSortByRelation() { $list = DataObjectTest_TeamComment::get(); @@ -874,12 +879,12 @@ class DataListTest extends SapphireTest { $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $list->last()->TeamID, 'Last comment should be for Team 1'); } - + public function testReverse() { $list = DataObjectTest_TeamComment::get(); $list = $list->sort('Name'); $list = $list->reverse(); - + $this->assertEquals('Bob', $list->last()->Name, 'Last comment should be from Bob'); $this->assertEquals('Phil', $list->first()->Name, 'First comment should be from Phil'); } From 97b226abe023bbc059633cd944ff04c281a675a8 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 30 Jul 2015 14:24:41 +1200 Subject: [PATCH 21/22] BUG Fix semver violation in create_table_options --- model/connect/MySQLSchemaManager.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/model/connect/MySQLSchemaManager.php b/model/connect/MySQLSchemaManager.php index 2490dcca6..5881e2426 100644 --- a/model/connect/MySQLSchemaManager.php +++ b/model/connect/MySQLSchemaManager.php @@ -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"; } From dd0e2dc36200e3931923693c2b9dc978aea99825 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 2 Apr 2015 11:27:33 +0100 Subject: [PATCH 22/22] FIX Image_Cached exists method doesnt check for positive ID FIX Files should only report as "existing" if the file actually exists --- filesystem/File.php | 11 +++- model/Image.php | 58 ++++++++++----------- tests/forms/uploadfield/UploadFieldTest.php | 41 ++++++++++++--- tests/forms/uploadfield/UploadFieldTest.yml | 20 +++---- 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/filesystem/File.php b/filesystem/File.php index 5bb4100f1..883975dff 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -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()); } } diff --git a/model/Image.php b/model/Image.php index 72e7d9a9c..9b14a715c 100644 --- a/model/Image.php +++ b/model/Image.php @@ -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'); } diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php index d9e42d029..b0be66152 100644 --- a/tests/forms/uploadfield/UploadFieldTest.php +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -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); } diff --git a/tests/forms/uploadfield/UploadFieldTest.yml b/tests/forms/uploadfield/UploadFieldTest.yml index fa8112d77..65a2832e7 100644 --- a/tests/forms/uploadfield/UploadFieldTest.yml +++ b/tests/forms/uploadfield/UploadFieldTest.yml @@ -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