From c02b4418bbab61a3c28178e75387d405f742425a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 27 Mar 2012 14:38:45 +1300 Subject: [PATCH 01/44] BUGFIX Using DateField "dmyfields" option, if you set empty day/month/year values, valueObj on DateField will contain erroneous values. Check that all the value inputs aren't null or empty values BEFORE calling Zend_Date on the value. --- forms/DateField.php | 9 +++++++-- tests/forms/DateFieldTest.php | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/forms/DateField.php b/forms/DateField.php index cc4c49615..a1f2c6b66 100644 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -196,8 +196,13 @@ class DateField extends TextField { // Setting in correct locale if(is_array($val) && $this->validateArrayValue($val)) { // set() gets confused with custom date formats when using array notation - $this->valueObj = new Zend_Date($val, null, $this->locale); - $this->value = $this->valueObj->toArray(); + if(!(empty($val['day']) || empty($val['month']) || empty($val['year']))) { + $this->valueObj = new Zend_Date($val, null, $this->locale); + $this->value = $this->valueObj->toArray(); + } else { + $this->value = $val; + $this->valueObj = null; + } } // load ISO date from database (usually through Form->loadDataForm()) else if(!empty($val) && Zend_Date::isDate($val, $this->getConfig('datavalueformat'), $this->locale)) { diff --git a/tests/forms/DateFieldTest.php b/tests/forms/DateFieldTest.php index 24d990cd2..5bb1c29e8 100644 --- a/tests/forms/DateFieldTest.php +++ b/tests/forms/DateFieldTest.php @@ -135,7 +135,18 @@ class DateFieldTest extends SapphireTest { // $f = new DateField('Date', 'Date', array('day' => 9999, 'month' => 9999, 'year' => 9999)); // $this->assertFalse($f->validate(new RequiredFields())); } - + + function testValidateEmptyArrayValuesSetsNullForValueObject() { + $f = new DateField('Date', 'Date'); + $f->setConfig('dmyfields', true); + + $f->setValue(array('day' => '', 'month' => '', 'year' => '')); + $this->assertNull($f->dataValue()); + + $f->setValue(array('day' => null, 'month' => null, 'year' => null)); + $this->assertNull($f->dataValue()); + } + function testValidateArrayValue() { $f = new DateField('Date', 'Date'); $this->assertTrue($f->validateArrayValue(array('day' => 29, 'month' => 03, 'year' => 2003))); From de2832e65f104c1833a1d32143998d47087d3456 Mon Sep 17 00:00:00 2001 From: Andrew O'Neil Date: Tue, 27 Mar 2012 17:57:42 +1300 Subject: [PATCH 02/44] ENHANCEMENT: Allow Object::create() to be called with late static binding. This allows DataList::create('SiteTree') as equivalent to Object::create('DataList', 'SiteTree'), without having to have a create() function on DataList. Required for E_STRICT compliance. --- admin/code/LeftAndMain.php | 4 +- core/Object.php | 16 +++++++- forms/FormAction.php | 4 -- forms/HiddenField.php | 5 --- forms/MoneyField.php | 2 +- forms/TableListField.php | 8 ++-- forms/ToggleField.php | 2 +- forms/gridfield/GridField.php | 4 +- model/ArrayList.php | 12 ------ model/DataList.php | 11 ------ model/DataObject.php | 6 +-- model/ManyManyList.php | 18 --------- model/fieldtypes/CompositeDBField.php | 4 +- model/fieldtypes/DBField.php | 9 ++++- model/fieldtypes/Datetime.php | 4 +- model/fieldtypes/Money.php | 4 +- tests/api/RestfulServerTest.php | 2 +- tests/core/ObjectTest.php | 5 +++ tests/dev/CsvBulkLoaderTest.php | 2 +- tests/model/DBFieldTest.php | 8 ++-- tests/model/DBLocaleTest.php | 6 +-- tests/model/DateTest.php | 56 +++++++++++++-------------- tests/model/DatetimeTest.php | 16 ++++---- tests/model/HTMLTextTest.php | 12 +++--- tests/model/StringFieldTest.php | 4 +- tests/model/TextTest.php | 14 +++---- 26 files changed, 107 insertions(+), 131 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 97c471a46..36b4174c6 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -467,7 +467,7 @@ class LeftAndMain extends Controller implements PermissionProvider { $menu->push(new ArrayData(array( "MenuItem" => $menuItem, "Title" => Convert::raw2xml($title), - "Code" => DBField::create('Text', $code), + "Code" => DBField::create_field('Text', $code), "Link" => $menuItem->url, "LinkingMode" => $linkingmode ))); @@ -1256,7 +1256,7 @@ class LeftAndMain extends Controller implements PermissionProvider { * @return String */ function Locale() { - return DBField::create('DBLocale', i18n::get_locale()); + return DBField::create_field('DBLocale', i18n::get_locale()); } function providePermissions() { diff --git a/core/Object.php b/core/Object.php index 85dbedb1e..145e36222 100755 --- a/core/Object.php +++ b/core/Object.php @@ -83,13 +83,27 @@ abstract class Object { * overload is found, an instance of this is returned rather than the original class. To overload a class, use * {@link Object::useCustomClass()} * + * This can be called in one of two ways - either calling via the class directly, + * or calling on Object and passing the class name as the first parameter. The following + * are equivalent: + * $list = DataList::create('SiteTree'); + * $list = Object::create('DataList', 'SiteTree'); + * * @param string $class the class name * @param mixed $arguments,... arguments to pass to the constructor * @return Object */ public static function create() { $args = func_get_args(); - $class = self::getCustomClass(array_shift($args)); + + // Class to create should be the calling class if not Object, + // otherwise the first parameter + $class = get_called_class(); + if($class == 'Object') + $class = array_shift($args); + + $class = self::getCustomClass($class); + $reflector = new ReflectionClass($class); if($reflector->getConstructor()) { return $reflector->newInstanceArgs($args); diff --git a/forms/FormAction.php b/forms/FormAction.php index 943f8c9b0..7e52f7c8f 100644 --- a/forms/FormAction.php +++ b/forms/FormAction.php @@ -45,10 +45,6 @@ class FormAction extends FormField { parent::__construct($this->action, $title, null, $form); } - static function create($action, $title = "") { - return new FormAction($action, $title); - } - function actionName() { return substr($this->name, 7); } diff --git a/forms/HiddenField.php b/forms/HiddenField.php index 1799992a3..1e33b080f 100644 --- a/forms/HiddenField.php +++ b/forms/HiddenField.php @@ -28,9 +28,4 @@ class HiddenField extends FormField { array('type' => 'hidden') ); } - - static function create($name) { - return new HiddenField($name); - } - } \ No newline at end of file diff --git a/forms/MoneyField.php b/forms/MoneyField.php index 4f879bb3f..b0ae5c78b 100644 --- a/forms/MoneyField.php +++ b/forms/MoneyField.php @@ -103,7 +103,7 @@ class MoneyField extends FormField { function saveInto($dataObject) { $fieldName = $this->name; if($dataObject->hasMethod("set$fieldName")) { - $dataObject->$fieldName = DBField::create('Money', array( + $dataObject->$fieldName = DBField::create_field('Money', array( "Currency" => $this->fieldCurrency->Value(), "Amount" => $this->fieldAmount->Value() )); diff --git a/forms/TableListField.php b/forms/TableListField.php index ca9657ae2..f20180d24 100644 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -637,8 +637,8 @@ JS $summaryFields[] = new ArrayData(array( 'Function' => $function, 'SummaryValue' => $summaryValue, - 'Name' => DBField::create('Varchar', $fieldName), - 'Title' => DBField::create('Varchar', $fieldTitle), + 'Name' => DBField::create_field('Varchar', $fieldName), + 'Title' => DBField::create_field('Varchar', $fieldTitle), )); } return new ArrayList($summaryFields); @@ -1234,13 +1234,13 @@ JS } if(strpos($castingDefinition,'->') === false) { $castingFieldType = $castingDefinition; - $castingField = DBField::create($castingFieldType, $value); + $castingField = DBField::create_field($castingFieldType, $value); $value = call_user_func_array(array($castingField,'XML'),$castingParams); } else { $fieldTypeParts = explode('->', $castingDefinition); $castingFieldType = $fieldTypeParts[0]; $castingMethod = $fieldTypeParts[1]; - $castingField = DBField::create($castingFieldType, $value); + $castingField = DBField::create_field($castingFieldType, $value); $value = call_user_func_array(array($castingField,$castingMethod),$castingParams); } diff --git a/forms/ToggleField.php b/forms/ToggleField.php index d0be2cdbe..0e23e3904 100644 --- a/forms/ToggleField.php +++ b/forms/ToggleField.php @@ -59,7 +59,7 @@ class ToggleField extends ReadonlyField { $rawInput = Convert::html2raw($valforInput); if($this->charNum) $reducedVal = substr($rawInput,0,$this->charNum); - else $reducedVal = DBField::create('Text',$rawInput)->{$this->truncateMethod}(); + else $reducedVal = DBField::create_field('Text',$rawInput)->{$this->truncateMethod}(); // only create togglefield if the truncated content is shorter if(strlen($reducedVal) < strlen($rawInput)) { diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php index 4548f8f9f..ed342adf5 100755 --- a/forms/gridfield/GridField.php +++ b/forms/gridfield/GridField.php @@ -239,13 +239,13 @@ class GridField extends FormField { if(strpos($castingDefinition,'->') === false) { $castingFieldType = $castingDefinition; - $castingField = DBField::create($castingFieldType, $value); + $castingField = DBField::create_field($castingFieldType, $value); $value = call_user_func_array(array($castingField,'XML'),$castingParams); } else { $fieldTypeParts = explode('->', $castingDefinition); $castingFieldType = $fieldTypeParts[0]; $castingMethod = $fieldTypeParts[1]; - $castingField = DBField::create($castingFieldType, $value); + $castingField = DBField::create_field($castingFieldType, $value); $value = call_user_func_array(array($castingField,$castingMethod),$castingParams); } diff --git a/model/ArrayList.php b/model/ArrayList.php index fe7bd91f2..7a11a629a 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -14,18 +14,6 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta */ protected $items; - - /** - * Synonym of the constructor. Can be chained with literate methods. - * ArrayList::create("SiteTree")->sort("Title") is legal, but - * new ArrayList("SiteTree")->sort("Title") is not. - * - * @param array $items - an initial array to fill this object with - */ - public static function create(array $items = array()) { - return new ArrayList($items); - } - /** * * @param array $items - an initial array to fill this object with diff --git a/model/DataList.php b/model/DataList.php index 1984af0df..bd1dad4da 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -27,17 +27,6 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * @var DataModel */ protected $model; - - /** - * Synonym of the constructor. Can be chained with literate methods. - * DataList::create("SiteTree")->sort("Title") is legal, but - * new DataList("SiteTree")->sort("Title") is not. - * - * @param string $dataClass - The DataObject class to query. - */ - public static function create($dataClass) { - return new DataList($dataClass); - } /** * Create a new DataList. diff --git a/model/DataObject.php b/model/DataObject.php index 24c98d805..d8eb2548a 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1092,7 +1092,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // if database column doesn't correlate to a DBField instance... if(!$fieldObj) { - $fieldObj = DBField::create('Varchar', $this->record[$fieldName], $fieldName); + $fieldObj = DBField::create_field('Varchar', $this->record[$fieldName], $fieldName); } // Both CompositeDBFields and regular fields need to be repopulated @@ -2380,12 +2380,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Special case for has_one relationships } else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) { $val = (isset($this->record[$fieldName])) ? $this->record[$fieldName] : null; - return DBField::create('ForeignKey', $val, $fieldName, $this); + return DBField::create_field('ForeignKey', $val, $fieldName, $this); // Special case for ClassName } else if($fieldName == 'ClassName') { $val = get_class($this); - return DBField::create('Varchar', $val, $fieldName, $this); + return DBField::create_field('Varchar', $val, $fieldName, $this); } } diff --git a/model/ManyManyList.php b/model/ManyManyList.php index ebb3048ee..676018c6f 100644 --- a/model/ManyManyList.php +++ b/model/ManyManyList.php @@ -12,24 +12,6 @@ class ManyManyList extends RelationList { protected $foreignKey, $foreignID; protected $extraFields; - - /** - * Synonym of the constructor. Can be chained with literate methods. - * ManyManyList::create("Group","Member","ID", "GroupID")->sort("Title") is legal, but - * new ManyManyList("Group","Member","ID", "GroupID")->sort("Title") is not. - * - * @param string $dataClass The class of the DataObjects that this will list. - * @param string $joinTable The name of the table whose entries define the content of this many_many relation. - * @param string $localKey The key in the join table that maps to the dataClass' PK. - * @param string $foreignKey The key in the join table that maps to joined class' PK. - * @param string $extraFields A map of field => fieldtype of extra fields on the join table. - * - * @see ManyManyList::__construct(); - * @example ManyManyList::create('Group','Group_Members', 'GroupID', 'MemberID'); - */ - public static function create($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) { - return new ManyManyList($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()); - } /** * Create a new ManyManyList object. diff --git a/model/fieldtypes/CompositeDBField.php b/model/fieldtypes/CompositeDBField.php index 9418a6421..b562fb99f 100644 --- a/model/fieldtypes/CompositeDBField.php +++ b/model/fieldtypes/CompositeDBField.php @@ -25,13 +25,13 @@ * if($this->getStreetName()) { * $manipulation['fields']["{$this->name}Name"] = $this->prepValueForDB($this->getStreetName()); * } else { -* $manipulation['fields']["{$this->name}Name"] = DBField::create('Varchar', $this->getStreetName())->nullValue(); +* $manipulation['fields']["{$this->name}Name"] = DBField::create_field('Varchar', $this->getStreetName())->nullValue(); * } * * if($this->getStreetNumber()) { * $manipulation['fields']["{$this->name}Number"] = $this->prepValueForDB($this->getStreetNumber()); * } else { -* $manipulation['fields']["{$this->name}Number"] = DBField::create('Int', $this->getStreetNumber())->nullValue(); +* $manipulation['fields']["{$this->name}Number"] = DBField::create_field('Int', $this->getStreetNumber())->nullValue(); * } * } * diff --git a/model/fieldtypes/DBField.php b/model/fieldtypes/DBField.php index 8769099b2..c0c1e9587 100644 --- a/model/fieldtypes/DBField.php +++ b/model/fieldtypes/DBField.php @@ -63,11 +63,18 @@ abstract class DBField extends ViewableData { parent::__construct(); } + + static function create() { + Deprecation::notice('3.0', 'DBField::create_field() is deprecated as it clashes with Object::create(). Use DBField::create_field() instead.'); + + return call_user_func_array(array('DBField', 'create_field'), func_get_args()); + } + /** * Create a DBField object that's not bound to any particular field. * Useful for accessing the classes behaviour for other parts of your code. */ - static function create($className, $value, $name = null, $object = null) { + static function create_field($className, $value, $name = null, $object = null) { $dbField = Object::create($className, $name, $object); $dbField->setValue($value, null, false); return $dbField; diff --git a/model/fieldtypes/Datetime.php b/model/fieldtypes/Datetime.php index ac4536877..626c70a41 100644 --- a/model/fieldtypes/Datetime.php +++ b/model/fieldtypes/Datetime.php @@ -96,7 +96,7 @@ class SS_Datetime extends Date { if(self::$mock_now) { return self::$mock_now; } else { - return DBField::create('SS_Datetime', date('Y-m-d H:i:s')); + return DBField::create_field('SS_Datetime', date('Y-m-d H:i:s')); } } @@ -111,7 +111,7 @@ class SS_Datetime extends Date { if($datetime instanceof SS_Datetime) { self::$mock_now = $datetime; } elseif(is_string($datetime)) { - self::$mock_now = DBField::create('SS_Datetime', $datetime); + self::$mock_now = DBField::create_field('SS_Datetime', $datetime); } else { throw new Exception('SS_Datetime::set_mock_now(): Wrong format: ' . $datetime); } diff --git a/model/fieldtypes/Money.php b/model/fieldtypes/Money.php index d654068a0..14c6a33ba 100644 --- a/model/fieldtypes/Money.php +++ b/model/fieldtypes/Money.php @@ -84,13 +84,13 @@ class Money extends DBField implements CompositeDBField { if($this->getCurrency()) { $manipulation['fields'][$this->name.'Currency'] = $this->prepValueForDB($this->getCurrency()); } else { - $manipulation['fields'][$this->name.'Currency'] = DBField::create('Varchar', $this->getCurrency())->nullValue(); + $manipulation['fields'][$this->name.'Currency'] = DBField::create_field('Varchar', $this->getCurrency())->nullValue(); } if($this->getAmount()) { $manipulation['fields'][$this->name.'Amount'] = $this->getAmount(); } else { - $manipulation['fields'][$this->name.'Amount'] = DBField::create('Decimal', $this->getAmount())->nullValue(); + $manipulation['fields'][$this->name.'Amount'] = DBField::create_field('Decimal', $this->getAmount())->nullValue(); } } diff --git a/tests/api/RestfulServerTest.php b/tests/api/RestfulServerTest.php index ed9b519fb..7980a18c2 100644 --- a/tests/api/RestfulServerTest.php +++ b/tests/api/RestfulServerTest.php @@ -544,7 +544,7 @@ class RestfulServerTest_AuthorRating extends DataObject implements TestOnly { static $db = array( 'Rating' => 'Int', 'SecretField' => 'Text', - 'WriteProtectedField' => 'Text' + 'WriteProtectedField' => 'Text', ); static $has_one = array( diff --git a/tests/core/ObjectTest.php b/tests/core/ObjectTest.php index 1bec01977..2aaa4fd0e 100644 --- a/tests/core/ObjectTest.php +++ b/tests/core/ObjectTest.php @@ -119,6 +119,11 @@ class ObjectTest extends SapphireTest { $strongObj = Object::strong_create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5'); $this->assertEquals($strongObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5')); } + + public function testCreateLateStaticBinding() { + $createdObj = ObjectTest_CreateTest::create('arg1', 'arg2', array(), null, 'arg5'); + $this->assertEquals($createdObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5')); + } /** * Tests that {@link Object::useCustomClass()} correnctly replaces normal and strong objects diff --git a/tests/dev/CsvBulkLoaderTest.php b/tests/dev/CsvBulkLoaderTest.php index c1fcf9dcd..23a30bb39 100644 --- a/tests/dev/CsvBulkLoaderTest.php +++ b/tests/dev/CsvBulkLoaderTest.php @@ -131,7 +131,7 @@ class CsvBulkLoaderTest extends SapphireTest { $this->assertEquals($testPlayer->ContractID, $testContract->ID, 'Creating new has_one relation works'); // Test nested setting of relation properties - $contractAmount = DBField::create('Currency', $compareRow[5])->RAW(); + $contractAmount = DBField::create_field('Currency', $compareRow[5])->RAW(); $this->assertEquals($testPlayer->Contract()->Amount, $contractAmount, 'Setting nested values in a relation works'); fclose($file); diff --git a/tests/model/DBFieldTest.php b/tests/model/DBFieldTest.php index b62988d6d..b6f3f7d29 100644 --- a/tests/model/DBFieldTest.php +++ b/tests/model/DBFieldTest.php @@ -202,7 +202,7 @@ class DBFieldTest extends SapphireTest { $value = 'üåäöÜÅÄÖ'; foreach ($allFields as $stringField) { - $stringField = DBField::create($stringField, $value); + $stringField = DBField::create_field($stringField, $value); for ($i = 1; $i < mb_strlen($value); $i++) { $expected = mb_substr($value, 0, $i) . '...'; $this->assertEquals($expected, $stringField->LimitCharacters($i)); @@ -211,12 +211,12 @@ class DBFieldTest extends SapphireTest { $value = '

üåäö&ÜÅÄÖ

'; foreach ($htmlFields as $stringField) { - $stringField = DBField::create($stringField, $value); + $stringField = DBField::create_field($stringField, $value); $this->assertEquals('üåäö&ÜÅÄ...', $stringField->LimitCharacters(8)); } - $this->assertEquals('ÅÄÖ', DBField::create('Text', 'åäö')->UpperCase()); - $this->assertEquals('åäö', DBField::create('Text', 'ÅÄÖ')->LowerCase()); + $this->assertEquals('ÅÄÖ', DBField::create_field('Text', 'åäö')->UpperCase()); + $this->assertEquals('åäö', DBField::create_field('Text', 'ÅÄÖ')->LowerCase()); } } diff --git a/tests/model/DBLocaleTest.php b/tests/model/DBLocaleTest.php index eeb435cbf..fa3a7a7d5 100644 --- a/tests/model/DBLocaleTest.php +++ b/tests/model/DBLocaleTest.php @@ -5,17 +5,17 @@ */ class DBLocaleTest extends SapphireTest { function testNice() { - $l = DBField::create('DBLocale', 'de_DE'); + $l = DBField::create_field('DBLocale', 'de_DE'); $this->assertEquals($l->Nice(), 'German'); } function testNiceNative() { - $l = DBField::create('DBLocale', 'de_DE'); + $l = DBField::create_field('DBLocale', 'de_DE'); $this->assertEquals($l->Nice(true), 'Deutsch'); } function testNativeName() { - $l = DBField::create('DBLocale', 'de_DE'); + $l = DBField::create_field('DBLocale', 'de_DE'); $this->assertEquals($l->getNativeName(), 'Deutsch'); } } diff --git a/tests/model/DateTest.php b/tests/model/DateTest.php index f09da2c86..196618603 100644 --- a/tests/model/DateTest.php +++ b/tests/model/DateTest.php @@ -32,96 +32,96 @@ class DateTest extends SapphireTest { } function testNiceDate() { - $this->assertEquals('01/04/2008', DBField::create('Date', 1206968400)->Nice(), + $this->assertEquals('01/04/2008', DBField::create_field('Date', 1206968400)->Nice(), "Date->Nice() works with timestamp integers" ); - $this->assertEquals('31/03/2008', DBField::create('Date', 1206882000)->Nice(), + $this->assertEquals('31/03/2008', DBField::create_field('Date', 1206882000)->Nice(), "Date->Nice() works with timestamp integers" ); - $this->assertEquals('01/04/2008', DBField::create('Date', '1206968400')->Nice(), + $this->assertEquals('01/04/2008', DBField::create_field('Date', '1206968400')->Nice(), "Date->Nice() works with timestamp strings" ); - $this->assertEquals('31/03/2008', DBField::create('Date', '1206882000')->Nice(), + $this->assertEquals('31/03/2008', DBField::create_field('Date', '1206882000')->Nice(), "Date->Nice() works with timestamp strings" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '4/3/03')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/03')->Nice(), "Date->Nice() works with D/M/YY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '04/03/03')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '04/03/03')->Nice(), "Date->Nice() works with DD/MM/YY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '4/3/03')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/03')->Nice(), "Date->Nice() works with D/M/YY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '4/03/03')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/03/03')->Nice(), "Date->Nice() works with D/M/YY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '4/3/2003')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '4/3/2003')->Nice(), "Date->Nice() works with D/M/YYYY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '4-3-2003')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '4-3-2003')->Nice(), "Date->Nice() works with D-M-YYYY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '2003-03-04')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '2003-03-04')->Nice(), "Date->Nice() works with YYYY-MM-DD format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '04/03/2003')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '04/03/2003')->Nice(), "Date->Nice() works with DD/MM/YYYY format" ); - $this->assertEquals('04/03/2003', DBField::create('Date', '04-03-2003')->Nice(), + $this->assertEquals('04/03/2003', DBField::create_field('Date', '04-03-2003')->Nice(), "Date->Nice() works with DD/MM/YYYY format" ); } function testLongDate() { - $this->assertEquals('1 April 2008', DBField::create('Date', 1206968400)->Long(), + $this->assertEquals('1 April 2008', DBField::create_field('Date', 1206968400)->Long(), "Date->Long() works with numeric timestamp" ); - $this->assertEquals('1 April 2008', DBField::create('Date', '1206968400')->Long(), + $this->assertEquals('1 April 2008', DBField::create_field('Date', '1206968400')->Long(), "Date->Long() works with string timestamp" ); - $this->assertEquals('31 March 2008', DBField::create('Date', 1206882000)->Long(), + $this->assertEquals('31 March 2008', DBField::create_field('Date', 1206882000)->Long(), "Date->Long() works with numeric timestamp" ); - $this->assertEquals('31 March 2008', DBField::create('Date', '1206882000')->Long(), + $this->assertEquals('31 March 2008', DBField::create_field('Date', '1206882000')->Long(), "Date->Long() works with numeric timestamp" ); - $this->assertEquals('3 April 2003', DBField::create('Date', '2003-4-3')->Long(), + $this->assertEquals('3 April 2003', DBField::create_field('Date', '2003-4-3')->Long(), "Date->Long() works with YYYY-M-D" ); - $this->assertEquals('3 April 2003', DBField::create('Date', '3/4/2003')->Long(), + $this->assertEquals('3 April 2003', DBField::create_field('Date', '3/4/2003')->Long(), "Date->Long() works with D/M/YYYY" ); } function testSetNullAndZeroValues() { - $date = DBField::create('Date', ''); + $date = DBField::create_field('Date', ''); $this->assertNull($date->getValue(), 'Empty string evaluates to NULL'); - $date = DBField::create('Date', null); + $date = DBField::create_field('Date', null); $this->assertNull($date->getValue(), 'NULL is set as NULL'); - $date = DBField::create('Date', false); + $date = DBField::create_field('Date', false); $this->assertNull($date->getValue(), 'Boolean FALSE evaluates to NULL'); - $date = DBField::create('Date', array()); + $date = DBField::create_field('Date', array()); $this->assertNull($date->getValue(), 'Empty array evaluates to NULL'); - $date = DBField::create('Date', '0'); + $date = DBField::create_field('Date', '0'); $this->assertEquals('1970-01-01', $date->getValue(), 'Zero is UNIX epoch date'); - $date = DBField::create('Date', 0); + $date = DBField::create_field('Date', 0); $this->assertEquals('1970-01-01', $date->getValue(), 'Zero is UNIX epoch date'); } function testDayOfMonth() { - $date = DBField::create('Date', '2000-10-10'); + $date = DBField::create_field('Date', '2000-10-10'); $this->assertEquals('10', $date->DayOfMonth()); $this->assertEquals('10th', $date->DayOfMonth(true)); - $range = $date->RangeString(DBField::create('Date', '2000-10-20')); + $range = $date->RangeString(DBField::create_field('Date', '2000-10-20')); $this->assertEquals('10 - 20 Oct 2000', $range); - $range = $date->RangeString(DBField::create('Date', '2000-10-20'), true); + $range = $date->RangeString(DBField::create_field('Date', '2000-10-20'), true); $this->assertEquals('10th - 20th Oct 2000', $range); } } diff --git a/tests/model/DatetimeTest.php b/tests/model/DatetimeTest.php index 910751b63..4939d57ec 100644 --- a/tests/model/DatetimeTest.php +++ b/tests/model/DatetimeTest.php @@ -12,7 +12,7 @@ */ class SS_DatetimeTest extends SapphireTest { function testNowWithSystemDate() { - $systemDatetime = DBField::create('SS_Datetime', date('Y-m-d H:i:s')); + $systemDatetime = DBField::create_field('SS_Datetime', date('Y-m-d H:i:s')); $nowDatetime = SS_Datetime::now(); $this->assertEquals($systemDatetime->Date(), $nowDatetime->Date()); @@ -22,14 +22,14 @@ class SS_DatetimeTest extends SapphireTest { // Test setting $mockDate = '2001-12-31 22:10:59'; SS_Datetime::set_mock_now($mockDate); - $systemDatetime = DBField::create('SS_Datetime', date('Y-m-d H:i:s')); + $systemDatetime = DBField::create_field('SS_Datetime', date('Y-m-d H:i:s')); $nowDatetime = SS_Datetime::now(); $this->assertNotEquals($systemDatetime->Date(), $nowDatetime->Date()); $this->assertEquals($nowDatetime->getValue(), $mockDate); // Test clearing SS_Datetime::clear_mock_now(); - $systemDatetime = DBField::create('SS_Datetime', date('Y-m-d H:i:s')); + $systemDatetime = DBField::create_field('SS_Datetime', date('Y-m-d H:i:s')); $nowDatetime = SS_Datetime::now(); $this->assertEquals($systemDatetime->Date(), $nowDatetime->Date()); } @@ -37,19 +37,19 @@ class SS_DatetimeTest extends SapphireTest { function testSetNullAndZeroValues() { date_default_timezone_set('UTC'); - $date = DBField::create('SS_Datetime', ''); + $date = DBField::create_field('SS_Datetime', ''); $this->assertNull($date->getValue(), 'Empty string evaluates to NULL'); - $date = DBField::create('SS_Datetime', null); + $date = DBField::create_field('SS_Datetime', null); $this->assertNull($date->getValue(), 'NULL is set as NULL'); - $date = DBField::create('SS_Datetime', false); + $date = DBField::create_field('SS_Datetime', false); $this->assertNull($date->getValue(), 'Boolean FALSE evaluates to NULL'); - $date = DBField::create('SS_Datetime', '0'); + $date = DBField::create_field('SS_Datetime', '0'); $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'String zero is UNIX epoch time'); - $date = DBField::create('SS_Datetime', 0); + $date = DBField::create_field('SS_Datetime', 0); $this->assertEquals('1970-01-01 00:00:00', $date->getValue(), 'Numeric zero is UNIX epoch time'); } diff --git a/tests/model/HTMLTextTest.php b/tests/model/HTMLTextTest.php index 62a92e5ca..235ee0ea9 100644 --- a/tests/model/HTMLTextTest.php +++ b/tests/model/HTMLTextTest.php @@ -108,30 +108,30 @@ class HTMLTextTest extends SapphireTest { } public function testRAW() { - $data = DBField::create('HTMLText', 'This & This'); + $data = DBField::create_field('HTMLText', 'This & This'); $this->assertEquals($data->RAW(), 'This & This'); - $data = DBField::create('HTMLText', 'This & This'); + $data = DBField::create_field('HTMLText', 'This & This'); $this->assertEquals($data->RAW(), 'This & This'); } public function testXML() { - $data = DBField::create('HTMLText', 'This & This'); + $data = DBField::create_field('HTMLText', 'This & This'); $this->assertEquals($data->XML(), 'This & This'); } public function testHTML() { - $data = DBField::create('HTMLText', 'This & This'); + $data = DBField::create_field('HTMLText', 'This & This'); $this->assertEquals($data->HTML(), 'This & This'); } public function testJS() { - $data = DBField::create('HTMLText', '"this is a test"'); + $data = DBField::create_field('HTMLText', '"this is a test"'); $this->assertEquals($data->JS(), '\"this is a test\"'); } public function testATT() { - $data = DBField::create('HTMLText', '"this is a test"'); + $data = DBField::create_field('HTMLText', '"this is a test"'); $this->assertEquals($data->ATT(), '"this is a test"'); } } diff --git a/tests/model/StringFieldTest.php b/tests/model/StringFieldTest.php index 2bfd9b8f2..2604da2e6 100644 --- a/tests/model/StringFieldTest.php +++ b/tests/model/StringFieldTest.php @@ -12,7 +12,7 @@ class StringFieldTest extends SapphireTest { function testLowerCase() { $this->assertEquals( 'this is a test!', - DBField::create('StringFieldTest_MyStringField', 'This is a TEST!')->LowerCase() + DBField::create_field('StringFieldTest_MyStringField', 'This is a TEST!')->LowerCase() ); } @@ -22,7 +22,7 @@ class StringFieldTest extends SapphireTest { function testUpperCase() { $this->assertEquals( 'THIS IS A TEST!', - DBField::create('StringFieldTest_MyStringField', 'This is a TEST!')->UpperCase() + DBField::create_field('StringFieldTest_MyStringField', 'This is a TEST!')->UpperCase() ); } diff --git a/tests/model/TextTest.php b/tests/model/TextTest.php index 4c8494311..bff8e329e 100644 --- a/tests/model/TextTest.php +++ b/tests/model/TextTest.php @@ -99,7 +99,7 @@ class TextTest extends SapphireTest { ); foreach($cases as $originalValue => $expectedValue) { - $textObj = DBField::create('Text', $originalValue); + $textObj = DBField::create_field('Text', $originalValue); $this->assertEquals($expectedValue, $textObj->BigSummary(4)); } } @@ -115,7 +115,7 @@ class TextTest extends SapphireTest { $testKeyword3 = 'a'; $testKeyword3a = 'ate'; - $textObj = DBField::create('Text', $testString1, 'Text'); + $textObj = DBField::create_field('Text', $testString1, 'Text'); $this->assertEquals( '... text. It is a test...', @@ -145,27 +145,27 @@ class TextTest extends SapphireTest { } public function testRAW() { - $data = DBField::create('Text', 'This & This'); + $data = DBField::create_field('Text', 'This & This'); $this->assertEquals($data->RAW(), 'This & This'); } public function testXML() { - $data = DBField::create('Text', 'This & This'); + $data = DBField::create_field('Text', 'This & This'); $this->assertEquals($data->XML(), 'This & This'); } public function testHTML() { - $data = DBField::create('Text', 'This & This'); + $data = DBField::create_field('Text', 'This & This'); $this->assertEquals($data->HTML(), 'This & This'); } public function testJS() { - $data = DBField::create('Text', '"this is a test"'); + $data = DBField::create_field('Text', '"this is a test"'); $this->assertEquals($data->JS(), '\"this is a test\"'); } public function testATT() { - $data = DBField::create('Text', '"this is a test"'); + $data = DBField::create_field('Text', '"this is a test"'); $this->assertEquals($data->ATT(), '"this is a test"'); } } \ No newline at end of file From cf014dc56d38868fda09e1466845aa8c8a3439f4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 27 Mar 2012 22:54:13 +1300 Subject: [PATCH 03/44] MINOR Replaced use of deprecated split() with preg_split() and fixed use of "&new Class()" which is deprecated in PHP 5.3 ENHANCEMENT E_DEPRECATED and E_USER_DEPRECATED are now handled as notice level errors in Debug. --- admin/code/CMSBatchActionHandler.php | 8 ++++---- core/Core.php | 2 +- core/Diff.php | 2 +- dev/Debug.php | 1 + dev/DebugView.php | 4 ++++ thirdparty/simpletest/form.php | 6 +++--- thirdparty/simpletest/http.php | 14 +++++++------- thirdparty/simpletest/page.php | 10 +++++----- thirdparty/simpletest/parser.php | 6 +++--- thirdparty/simpletest/url.php | 8 ++++---- 10 files changed, 33 insertions(+), 28 deletions(-) diff --git a/admin/code/CMSBatchActionHandler.php b/admin/code/CMSBatchActionHandler.php index f45533ba9..4bc6c4f3b 100644 --- a/admin/code/CMSBatchActionHandler.php +++ b/admin/code/CMSBatchActionHandler.php @@ -81,7 +81,7 @@ class CMSBatchActionHandler extends RequestHandler { $actionHandler = new $actionClass(); // Sanitise ID list and query the database for apges - $ids = split(' *, *', trim($request->requestVar('csvIDs'))); + $ids = preg_split('/ *, */', trim($request->requestVar('csvIDs'))); foreach($ids as $k => $v) if(!is_numeric($v)) unset($ids[$k]); if($ids) { @@ -135,7 +135,7 @@ class CMSBatchActionHandler extends RequestHandler { $actionHandler = new $actionClass['class'](); // Sanitise ID list and query the database for apges - $ids = split(' *, *', trim($request->requestVar('csvIDs'))); + $ids = preg_split('/ *, */', trim($request->requestVar('csvIDs'))); foreach($ids as $k => $id) $ids[$k] = (int)$id; $ids = array_filter($ids); @@ -157,7 +157,7 @@ class CMSBatchActionHandler extends RequestHandler { $actionHandler = new $actionClass(); // Sanitise ID list and query the database for apges - $ids = split(' *, *', trim($request->requestVar('csvIDs'))); + $ids = preg_split('/ *, */', trim($request->requestVar('csvIDs'))); foreach($ids as $k => $id) $ids[$k] = (int)$id; $ids = array_filter($ids); @@ -211,4 +211,4 @@ class CMSBatchActionHandler extends RequestHandler { return $actions; } -} \ No newline at end of file +} diff --git a/core/Core.php b/core/Core.php index 04f8fdf1f..622655494 100644 --- a/core/Core.php +++ b/core/Core.php @@ -38,7 +38,7 @@ /////////////////////////////////////////////////////////////////////////////// // ENVIRONMENT CONFIG -if(defined('E_DEPRECATED')) error_reporting(E_ALL & ~(E_DEPRECATED | E_STRICT)); +if(defined('E_DEPRECATED')) error_reporting(E_ALL & ~(E_STRICT)); else error_reporting(E_ALL); /** diff --git a/core/Diff.php b/core/Diff.php index f59da6705..8623f8b14 100644 --- a/core/Diff.php +++ b/core/Diff.php @@ -800,7 +800,7 @@ class Diff if(is_array($content)) $content = implode(',', $content); $content = str_replace(array(" ","<", ">"),array(" "," <", "> "),$content); - $candidateChunks = split("[\t\r\n ]+", $content); + $candidateChunks = preg_split("/[\t\r\n ]+/", $content); while(list($i,$item) = each($candidateChunks)) { if(isset($item[0]) && $item[0] == "<") { $newChunk = $item; diff --git a/dev/Debug.php b/dev/Debug.php index b2d51b9a8..847b3bedd 100644 --- a/dev/Debug.php +++ b/dev/Debug.php @@ -697,6 +697,7 @@ function errorHandler($errno, $errstr, $errfile, $errline) { case E_NOTICE: case E_USER_NOTICE: + case E_DEPRECATED: case E_USER_DEPRECATED: Debug::noticeHandler($errno, $errstr, $errfile, $errline, null); break; diff --git a/dev/DebugView.php b/dev/DebugView.php index c6fa60b26..d29d9987a 100644 --- a/dev/DebugView.php +++ b/dev/DebugView.php @@ -30,6 +30,10 @@ class DebugView extends Object { 'title' => 'User Notice', 'class' => 'notice' ), + E_DEPRECATED => array( + 'title' => 'Deprecation', + 'class' => 'notice' + ), E_USER_DEPRECATED => array( 'title' => 'Deprecation', 'class' => 'notice' diff --git a/thirdparty/simpletest/form.php b/thirdparty/simpletest/form.php index cbef6636d..b359552f4 100644 --- a/thirdparty/simpletest/form.php +++ b/thirdparty/simpletest/form.php @@ -172,7 +172,7 @@ class SimpleForm { */ function _addRadioButton(&$tag) { if (! isset($this->_radios[$tag->getName()])) { - $this->_widgets[] = &new SimpleRadioGroup(); + $this->_widgets[] = new SimpleRadioGroup(); $this->_radios[$tag->getName()] = count($this->_widgets) - 1; } $this->_widgets[$this->_radios[$tag->getName()]]->addWidget($tag); @@ -191,7 +191,7 @@ class SimpleForm { $index = $this->_checkboxes[$tag->getName()]; if (! SimpleTestCompatibility::isA($this->_widgets[$index], 'SimpleCheckboxGroup')) { $previous = &$this->_widgets[$index]; - $this->_widgets[$index] = &new SimpleCheckboxGroup(); + $this->_widgets[$index] = new SimpleCheckboxGroup(); $this->_widgets[$index]->addWidget($previous); } $this->_widgets[$index]->addWidget($tag); @@ -352,4 +352,4 @@ class SimpleForm { return $this->_encode(); } } -?> \ No newline at end of file +?> diff --git a/thirdparty/simpletest/http.php b/thirdparty/simpletest/http.php index e6c6e89da..176d9fccd 100644 --- a/thirdparty/simpletest/http.php +++ b/thirdparty/simpletest/http.php @@ -98,9 +98,9 @@ class SimpleRoute { */ function &_createSocket($scheme, $host, $port, $timeout) { if (in_array($scheme, array('https'))) { - $socket = &new SimpleSecureSocket($host, $port, $timeout); + $socket = new SimpleSecureSocket($host, $port, $timeout); } else { - $socket = &new SimpleSocket($host, $port, $timeout); + $socket = new SimpleSocket($host, $port, $timeout); } return $socket; } @@ -279,7 +279,7 @@ class SimpleHttpRequest { * @access protected */ function &_createResponse(&$socket) { - $response = &new SimpleHttpResponse( + $response = new SimpleHttpResponse( $socket, $this->_route->getUrl(), $this->_encoding); @@ -516,13 +516,13 @@ class SimpleHttpResponse extends SimpleStickyError { function _parse($raw) { if (! $raw) { $this->_setError('Nothing fetched'); - $this->_headers = &new SimpleHttpHeaders(''); + $this->_headers = new SimpleHttpHeaders(''); } elseif (! strstr($raw, "\r\n\r\n")) { $this->_setError('Could not split headers from content'); - $this->_headers = &new SimpleHttpHeaders($raw); + $this->_headers = new SimpleHttpHeaders($raw); } else { list($headers, $this->_content) = split("\r\n\r\n", $raw, 2); - $this->_headers = &new SimpleHttpHeaders($headers); + $this->_headers = new SimpleHttpHeaders($headers); } } @@ -621,4 +621,4 @@ class SimpleHttpResponse extends SimpleStickyError { return ! $packet; } } -?> \ No newline at end of file +?> diff --git a/thirdparty/simpletest/page.php b/thirdparty/simpletest/page.php index 08e5649dc..6c037626c 100644 --- a/thirdparty/simpletest/page.php +++ b/thirdparty/simpletest/page.php @@ -163,7 +163,7 @@ class SimplePageBuilder extends SimpleSaxListener { * @access protected */ function &_createPage($response) { - $page = &new SimplePage($response); + $page = new SimplePage($response); return $page; } @@ -175,7 +175,7 @@ class SimplePageBuilder extends SimpleSaxListener { * @access protected */ function &_createParser(&$listener) { - $parser = &new SimpleHtmlSaxParser($listener); + $parser = new SimpleHtmlSaxParser($listener); return $parser; } @@ -188,7 +188,7 @@ class SimplePageBuilder extends SimpleSaxListener { * @access public */ function startElement($name, $attributes) { - $factory = &new SimpleTagBuilder(); + $factory = new SimpleTagBuilder(); $tag = $factory->createTag($name, $attributes); if (! $tag) { return true; @@ -641,7 +641,7 @@ class SimplePage { * @access public */ function acceptFormStart(&$tag) { - $this->_open_forms[] = &new SimpleForm($tag, $this); + $this->_open_forms[] = new SimpleForm($tag, $this); } /** @@ -980,4 +980,4 @@ class SimplePage { return null; } } -?> \ No newline at end of file +?> diff --git a/thirdparty/simpletest/parser.php b/thirdparty/simpletest/parser.php index 3f3b37b83..93f8cf980 100644 --- a/thirdparty/simpletest/parser.php +++ b/thirdparty/simpletest/parser.php @@ -197,7 +197,7 @@ class SimpleLexer { $this->_case = $case; $this->_regexes = array(); $this->_parser = &$parser; - $this->_mode = &new SimpleStateStack($start); + $this->_mode = new SimpleStateStack($start); $this->_mode_handlers = array($start => $start); } @@ -579,7 +579,7 @@ class SimpleHtmlSaxParser { * @static */ function &createLexer(&$parser) { - $lexer = &new SimpleHtmlLexer($parser); + $lexer = new SimpleHtmlLexer($parser); return $lexer; } @@ -761,4 +761,4 @@ class SimpleSaxListener { function addContent($text) { } } -?> \ No newline at end of file +?> diff --git a/thirdparty/simpletest/url.php b/thirdparty/simpletest/url.php index 0ea220409..d3461657a 100644 --- a/thirdparty/simpletest/url.php +++ b/thirdparty/simpletest/url.php @@ -106,7 +106,7 @@ class SimpleUrl { } if (preg_match('/^([^\/]*)@(.*)/', $url, $matches)) { $url = $prefix . $matches[2]; - $parts = split(":", $matches[1]); + $parts = preg_split('/:/', $matches[1]); return array( urldecode($parts[0]), isset($parts[1]) ? urldecode($parts[1]) : false); @@ -184,7 +184,7 @@ class SimpleUrl { function _parseRequest($raw) { $this->_raw = $raw; $request = new SimpleGetEncoding(); - foreach (split("&", $raw) as $pair) { + foreach (preg_split('/&/', $raw) as $pair) { if (preg_match('/(.*?)=(.*)/', $pair, $matches)) { $request->add($matches[1], urldecode($matches[2])); } elseif ($pair) { @@ -379,7 +379,7 @@ class SimpleUrl { */ function clearRequest() { $this->_raw = false; - $this->_request = &new SimpleGetEncoding(); + $this->_request = new SimpleGetEncoding(); } /** @@ -525,4 +525,4 @@ class SimpleUrl { return 'com|edu|net|org|gov|mil|int|biz|info|name|pro|aero|coop|museum'; } } -?> \ No newline at end of file +?> From 82bb12b5d3457a6ce5977d42cd0a78c715b70a88 Mon Sep 17 00:00:00 2001 From: Fred Condo Date: Sun, 11 Mar 2012 11:49:49 -0700 Subject: [PATCH 04/44] MINOR: Explicitly declare $adapter in DbDatetimeTest --- tests/model/DbDatetimeTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/model/DbDatetimeTest.php b/tests/model/DbDatetimeTest.php index 31ce19dfe..211100284 100644 --- a/tests/model/DbDatetimeTest.php +++ b/tests/model/DbDatetimeTest.php @@ -11,6 +11,8 @@ class DbDatetimeTest extends FunctionalTest { E_USER_ERROR => 1800, E_USER_NOTICE => 5, ); + + private $adapter; /** * Check if dates match more or less. This takes into the account the db query From 59547745303bda7d08171ebca089e0de38a20007 Mon Sep 17 00:00:00 2001 From: Fred Condo Date: Sun, 11 Mar 2012 09:59:09 -0700 Subject: [PATCH 05/44] BUGFIX: Use UTC consistently across all tests for date/time calculations This ensures that tests will not pass or fail based on whether the test machine is on NZ time. This partially reverts df050eda5d7c6ad6234d8ae7d94b46a4ddff8449, which has already been merged. Instead of finding tests that use date calculations, we are now setting the default time zone in SapphireTest so it will apply to the whole test suite and any future tests. Adjust expected values in certain tests for UTC, where the expected values had previously been expressed in NZ time. When creating a temp DB for test fixtures, create the DB connection with timezone UTC. --- dev/SapphireTest.php | 11 ++++++++++- tests/model/DateTest.php | 16 ++++++++-------- tests/model/DatetimeTest.php | 2 -- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 97c6114b5..823edcc6e 100644 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -143,6 +143,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase { i18n::set_date_format(null); i18n::set_time_format(null); + // Set default timezone consistently to avoid NZ-specific dependencies + date_default_timezone_set('UTC'); + // Remove password validation $this->originalMemberPasswordValidator = Member::password_validator(); $this->originalRequirements = Requirements::backend(); @@ -272,6 +275,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase { // which is used in DatabaseAdmin->doBuild() global $_SINGLETONS; $_SINGLETONS = array(); + + // Set default timezone consistently to avoid NZ-specific dependencies + date_default_timezone_set('UTC'); } /** @@ -765,7 +771,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase { // Disable PHPUnit error handling restore_error_handler(); - // Create a temporary database + // Create a temporary database, and force the connection to use UTC for time + global $databaseConfig; + $databaseConfig['timezone'] = '+0:00'; + DB::connect($databaseConfig); $dbConn = DB::getConn(); $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; $dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999); diff --git a/tests/model/DateTest.php b/tests/model/DateTest.php index f09da2c86..00a344a1f 100644 --- a/tests/model/DateTest.php +++ b/tests/model/DateTest.php @@ -32,16 +32,16 @@ class DateTest extends SapphireTest { } function testNiceDate() { - $this->assertEquals('01/04/2008', DBField::create('Date', 1206968400)->Nice(), + $this->assertEquals('31/03/2008', DBField::create('Date', 1206968400)->Nice(), "Date->Nice() works with timestamp integers" ); - $this->assertEquals('31/03/2008', DBField::create('Date', 1206882000)->Nice(), + $this->assertEquals('30/03/2008', DBField::create('Date', 1206882000)->Nice(), "Date->Nice() works with timestamp integers" ); - $this->assertEquals('01/04/2008', DBField::create('Date', '1206968400')->Nice(), + $this->assertEquals('31/03/2008', DBField::create('Date', '1206968400')->Nice(), "Date->Nice() works with timestamp strings" ); - $this->assertEquals('31/03/2008', DBField::create('Date', '1206882000')->Nice(), + $this->assertEquals('30/03/2008', DBField::create('Date', '1206882000')->Nice(), "Date->Nice() works with timestamp strings" ); $this->assertEquals('04/03/2003', DBField::create('Date', '4/3/03')->Nice(), @@ -74,16 +74,16 @@ class DateTest extends SapphireTest { } function testLongDate() { - $this->assertEquals('1 April 2008', DBField::create('Date', 1206968400)->Long(), + $this->assertEquals('31 March 2008', DBField::create('Date', 1206968400)->Long(), "Date->Long() works with numeric timestamp" ); - $this->assertEquals('1 April 2008', DBField::create('Date', '1206968400')->Long(), + $this->assertEquals('31 March 2008', DBField::create('Date', '1206968400')->Long(), "Date->Long() works with string timestamp" ); - $this->assertEquals('31 March 2008', DBField::create('Date', 1206882000)->Long(), + $this->assertEquals('30 March 2008', DBField::create('Date', 1206882000)->Long(), "Date->Long() works with numeric timestamp" ); - $this->assertEquals('31 March 2008', DBField::create('Date', '1206882000')->Long(), + $this->assertEquals('30 March 2008', DBField::create('Date', '1206882000')->Long(), "Date->Long() works with numeric timestamp" ); $this->assertEquals('3 April 2003', DBField::create('Date', '2003-4-3')->Long(), diff --git a/tests/model/DatetimeTest.php b/tests/model/DatetimeTest.php index 910751b63..5c1483e08 100644 --- a/tests/model/DatetimeTest.php +++ b/tests/model/DatetimeTest.php @@ -35,8 +35,6 @@ class SS_DatetimeTest extends SapphireTest { } function testSetNullAndZeroValues() { - date_default_timezone_set('UTC'); - $date = DBField::create('SS_Datetime', ''); $this->assertNull($date->getValue(), 'Empty string evaluates to NULL'); From 1a81c3de2712a21c014593c7b23b703b7328159b Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Wed, 28 Mar 2012 17:56:57 +1300 Subject: [PATCH 06/44] BUGFIX The SilverStripe installer throwing warning on settings that was legit. --- dev/install/install.php5 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dev/install/install.php5 b/dev/install/install.php5 index 5efee6e34..45c95f2d3 100644 --- a/dev/install/install.php5 +++ b/dev/install/install.php5 @@ -402,9 +402,9 @@ class InstallRequirements { $this->requireDateTimezone(array('PHP Configuration', 'date.timezone set and valid', 'date.timezone option in php.ini must be set in PHP 5.3.0+', ini_get('date.timezone'))); } - $this->suggestPHPSetting('asp_tags', array(''), array('PHP Configuration', 'asp_tags option turned off', 'This should be turned off as it can cause issues with SilverStripe')); - $this->suggestPHPSetting('magic_quotes_gpc', array(''), array('PHP Configuration', 'magic_quotes_gpc option turned off', 'This should be turned off, as it can cause issues with cookies. More specifically, unserializing data stored in cookies.')); - $this->suggestPHPSetting('display_errors', array(''), array('PHP Configuration', 'display_errors option turned off', 'Unless you\'re in a development environment, this should be turned off, as it can expose sensitive data to website users.')); + $this->suggestPHPSetting('asp_tags', array(false,0,''), array('PHP Configuration', 'asp_tags option turned off', 'This should be turned off as it can cause issues with SilverStripe')); + $this->suggestPHPSetting('magic_quotes_gpc', array(false,0,''), array('PHP Configuration', 'magic_quotes_gpc option turned off', 'This should be turned off, as it can cause issues with cookies. More specifically, unserializing data stored in cookies.')); + $this->suggestPHPSetting('display_errors', array(false,0,''), array('PHP Configuration', 'display_errors option turned off', 'Unless you\'re in a development environment, this should be turned off, as it can expose sensitive data to website users.')); // Check memory allocation $this->requireMemory(32*1024*1024, 64*1024*1024, array("PHP Configuration", "Memory allocated (PHP config option 'memory_limit')", "SilverStripe needs a minimum of 32M allocated to PHP, but recommends 64M.", ini_get("memory_limit"))); @@ -414,7 +414,6 @@ class InstallRequirements { function suggestPHPSetting($settingName, $settingValues, $testDetails) { $this->testing($testDetails); - $val = ini_get($settingName); if(!in_array($val, $settingValues) && $val != $settingValues) { $testDetails[2] = "$settingName is set to '$val' in php.ini. $testDetails[2]"; From e097f6e1a873615344ca027d2f953eea80bfc628 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 27 Mar 2012 17:04:11 +1300 Subject: [PATCH 07/44] MINOR Fixes to method arguments in core classes for E_STRICT support. API CHANGE Remove abstract static function and just use static functions in Authenticator (PHP 5.3+ doesn't support abstract static functions) --- admin/code/LeftAndMain.php | 2 +- admin/code/ModelAdmin.php | 2 +- core/manifest/ClassLoader.php | 2 +- filesystem/File.php | 6 +-- filesystem/Folder.php | 2 +- forms/TreeDropdownField.php | 4 +- model/Aggregate.php | 2 +- model/DataExtension.php | 2 +- model/DataObject.php | 4 +- model/Hierarchy.php | 6 +-- model/Image.php | 4 +- model/SQLQuery.php | 2 +- model/Versioned.php | 2 +- model/fieldtypes/Currency.php | 2 +- model/fieldtypes/Date.php | 2 +- model/fieldtypes/Datetime.php | 2 +- model/fieldtypes/HTMLText.php | 2 +- security/Authenticator.php | 11 ++-- security/Group.php | 2 +- security/Member.php | 2 +- security/PermissionRole.php | 4 +- tests/control/RequestHandlingTest.php | 4 +- tests/forms/FormScaffolderTest.php | 4 +- tests/forms/FormTest.php | 4 +- .../gridfield/GridFieldDetailFormTest.php | 12 ++--- tests/forms/uploadfield/UploadFieldTest.php | 10 ++-- tests/model/AggregateTest.php | 53 ++++++++++--------- tests/model/DataDifferencerTest.php | 6 +-- tests/security/GroupTest.php | 2 +- tests/security/MemberTest.php | 10 ++-- tests/view/SSViewerTest.php | 6 +-- 31 files changed, 93 insertions(+), 85 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 97c471a46..5993c9318 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -322,7 +322,7 @@ class LeftAndMain extends Controller implements PermissionProvider { SSViewer::set_theme(null); } - function handleRequest($request, DataModel $model) { + function handleRequest(SS_HTTPRequest $request, DataModel $model = null) { $title = $this->Title(); $response = parent::handleRequest($request, $model); diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 1c5a1834c..131f4e192 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -113,7 +113,7 @@ abstract class ModelAdmin extends LeftAndMain { Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ModelAdmin.js'); } - function getEditForm($id = null) { + function getEditForm($id = null, $fields = null) { $list = $this->getList(); $exportButton = new GridFieldExportButton(); $exportButton->setExportColumns($this->getExportFields()); diff --git a/core/manifest/ClassLoader.php b/core/manifest/ClassLoader.php index d91f31861..425fc9da7 100644 --- a/core/manifest/ClassLoader.php +++ b/core/manifest/ClassLoader.php @@ -85,4 +85,4 @@ class SS_ClassLoader { return class_exists($class, false) || $this->getManifest()->getItemPath($class); } -} \ No newline at end of file +} diff --git a/filesystem/File.php b/filesystem/File.php index 92d73e738..4a951b0cb 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -319,7 +319,7 @@ class File extends DataObject { * Returns the fields to power the edit screen of files in the CMS * @return FieldList */ - function getCMSFields() { + function getCMSFields($params = null) { // Preview if($this instanceof Image) { $formattedImage = $this->getFormattedImage('SetWidth', Image::$asset_preview_width); @@ -816,8 +816,8 @@ class File extends DataObject { } } - public function flushCache() { - parent::flushCache(); + public function flushCache($persistant = true) { + parent::flushCache($persistant); self::$cache_file_fields = null; } diff --git a/filesystem/Folder.php b/filesystem/Folder.php index 0a1c698b6..3d540e1b3 100644 --- a/filesystem/Folder.php +++ b/filesystem/Folder.php @@ -406,7 +406,7 @@ class Folder extends File { * You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension} * and implemeting updateCMSFields(FieldList $fields) on that extension. */ - function getCMSFields() { + function getCMSFields($param = null) { // Hide field on root level, which can't be renamed if(!$this->ID || $this->ID === "root") { $titleField = new HiddenField("Name"); diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 73282d113..2f62d7c8e 100644 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -343,7 +343,7 @@ class TreeDropdownField extends FormField { class TreeDropdownField_Readonly extends TreeDropdownField { protected $readonly = true; - function Field() { + function Field($properties = array()) { $fieldName = $this->labelField; if($this->value) { $keyObj = $this->objectForKey($this->value); @@ -361,4 +361,4 @@ class TreeDropdownField_Readonly extends TreeDropdownField { $field->setForm($this->form); return $field->Field(); } -} \ No newline at end of file +} diff --git a/model/Aggregate.php b/model/Aggregate.php index 77a483db3..e2fd134ff 100644 --- a/model/Aggregate.php +++ b/model/Aggregate.php @@ -90,7 +90,7 @@ class Aggregate extends ViewableData { * This gets the aggregate function * */ - public function XML_val($name, $args) { + public function XML_val($name, $args = null, $cache = false) { $func = strtoupper( strpos($name, 'get') === 0 ? substr($name, 3) : $name ); $attribute = $args ? $args[0] : 'ID'; diff --git a/model/DataExtension.php b/model/DataExtension.php index 614825997..7d8e9c172 100644 --- a/model/DataExtension.php +++ b/model/DataExtension.php @@ -131,7 +131,7 @@ abstract class DataExtension extends Extension { * @return array Returns a map where the keys are db, has_one, etc, and * the values are additional fields/relations to be defined. */ - function extraStatics($class=null, $extension=null) { + function extraStatics($class = null, $extension = null) { return array(); } diff --git a/model/DataObject.php b/model/DataObject.php index 451a9905b..4a5add52a 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -2654,7 +2654,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * When false will just clear session-local cached data * */ - public function flushCache($persistant=true) { + public function flushCache($persistant = true) { if($persistant) Aggregate::flushCache($this->class); if($this->class == 'DataObject') { @@ -3342,4 +3342,4 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } -} \ No newline at end of file +} diff --git a/model/Hierarchy.php b/model/Hierarchy.php index 20b33e49b..b8e988e3f 100644 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -674,12 +674,12 @@ class Hierarchy extends DataExtension { self::$expanded = array(); self::$treeOpened = array(); } - - function reset() { + + public static function reset() { self::$marked = array(); self::$expanded = array(); self::$treeOpened = array(); } + } - diff --git a/model/Image.php b/model/Image.php index f94ff443c..658bd5898 100644 --- a/model/Image.php +++ b/model/Image.php @@ -73,8 +73,8 @@ class Image extends File { parent::defineMethods(); } - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); $urlLink = "
"; $urlLink .= ""; diff --git a/model/SQLQuery.php b/model/SQLQuery.php index c283474a5..6d66e1cc9 100644 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -554,7 +554,7 @@ class SQLQuery { * Return the number of rows in this query if the limit were removed. Useful in paged data sets. * @return int */ - function unlimitedRowCount( $column = null) { + function unlimitedRowCount($column = null) { // we can't clear the select if we're relying on its output by a HAVING clause if(count($this->having)) { $records = $this->execute(); diff --git a/model/Versioned.php b/model/Versioned.php index fa446fafb..dc9a62500 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -132,7 +132,7 @@ class Versioned extends DataExtension { * Augment the the SQLQuery that is created by the DataQuery * @todo Should this all go into VersionedDataQuery? */ - function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery) { + function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) { $baseTable = ClassInfo::baseDataClass($dataQuery->dataClass()); switch($dataQuery->getQueryParam('Versioned.mode')) { diff --git a/model/fieldtypes/Currency.php b/model/fieldtypes/Currency.php index 23a10c734..5d0d2b6ec 100644 --- a/model/fieldtypes/Currency.php +++ b/model/fieldtypes/Currency.php @@ -41,7 +41,7 @@ class Currency extends Decimal { else return $val; } - function setValue($value) { + function setValue($value, $record = null) { $matches = null; if(is_numeric($value)) { $this->value = $value; diff --git a/model/fieldtypes/Date.php b/model/fieldtypes/Date.php index 60d3c834f..804fad3a1 100644 --- a/model/fieldtypes/Date.php +++ b/model/fieldtypes/Date.php @@ -20,7 +20,7 @@ */ class Date extends DBField { - function setValue($value) { + function setValue($value, $record = null) { if($value === false || $value === null || (is_string($value) && !strlen($value))) { // don't try to evaluate empty values with strtotime() below, as it returns "1970-01-01" when it should be saved as NULL in database $this->value = null; diff --git a/model/fieldtypes/Datetime.php b/model/fieldtypes/Datetime.php index ac4536877..b655eff5a 100644 --- a/model/fieldtypes/Datetime.php +++ b/model/fieldtypes/Datetime.php @@ -25,7 +25,7 @@ */ class SS_Datetime extends Date { - function setValue($value) { + function setValue($value, $record = null) { if($value === false || $value === null || (is_string($value) && !strlen($value))) { // don't try to evaluate empty values with strtotime() below, as it returns "1970-01-01" when it should be saved as NULL in database $this->value = null; diff --git a/model/fieldtypes/HTMLText.php b/model/fieldtypes/HTMLText.php index 046be3262..b90c98681 100644 --- a/model/fieldtypes/HTMLText.php +++ b/model/fieldtypes/HTMLText.php @@ -136,7 +136,7 @@ class HTMLText extends Text { return new HtmlEditorField($this->name, $title); } - public function scaffoldSearchField($title = null) { + public function scaffoldSearchField($title = null, $params = null) { return new TextField($this->name, $title); } diff --git a/security/Authenticator.php b/security/Authenticator.php index 0a226a89a..0d38b9e1c 100644 --- a/security/Authenticator.php +++ b/security/Authenticator.php @@ -37,9 +37,8 @@ abstract class Authenticator extends Object { * @return bool|Member Returns FALSE if authentication fails, otherwise * the member object */ - public abstract static function authenticate($RAW_data, - Form $form = null); - + public static function authenticate($RAW_data, Form $form = null) { + } /** * Method that creates the login form for this authentication method @@ -49,7 +48,8 @@ abstract class Authenticator extends Object { * @return Form Returns the login form to use with this authentication * method */ - public abstract static function get_login_form(Controller $controller); + public static function get_login_form(Controller $controller) { + } /** @@ -57,7 +57,8 @@ abstract class Authenticator extends Object { * * @return string Returns the name of the authentication method. */ - public abstract static function get_name(); + public static function get_name() { + } public static function register($authenticator) { self::register_authenticator($authenticator); diff --git a/security/Group.php b/security/Group.php index b4b8fde83..ff470aac2 100755 --- a/security/Group.php +++ b/security/Group.php @@ -59,7 +59,7 @@ class Group extends DataObject { * * @return FieldList */ - public function getCMSFields() { + public function getCMSFields($params = null) { Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js'); $fields = new FieldList( diff --git a/security/Member.php b/security/Member.php index b1e12a1c7..e118ae013 100644 --- a/security/Member.php +++ b/security/Member.php @@ -1084,7 +1084,7 @@ class Member extends DataObject implements TemplateGlobalProvider { * @return FieldList Return a FieldList of fields that would appropriate for * editing this member. */ - public function getCMSFields() { + public function getCMSFields($params = null) { require_once('Zend/Date.php'); $fields = parent::getCMSFields(); diff --git a/security/PermissionRole.php b/security/PermissionRole.php index 3f2dd0da1..3c1a00193 100644 --- a/security/PermissionRole.php +++ b/security/PermissionRole.php @@ -33,8 +33,8 @@ class PermissionRole extends DataObject { static $plural_name = 'Roles'; - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); $fields->removeFieldFromTab('Root', 'Codes'); $fields->removeFieldFromTab('Root', 'Groups'); diff --git a/tests/control/RequestHandlingTest.php b/tests/control/RequestHandlingTest.php index 72b62fe88..0454332a5 100644 --- a/tests/control/RequestHandlingTest.php +++ b/tests/control/RequestHandlingTest.php @@ -329,7 +329,7 @@ class RequestHandlingTest_Controller extends Controller implements TestOnly { $this->httpError(404, 'This page does not exist.'); } - public function getViewer(){ + public function getViewer($action) { return new SSViewer('BlankPage'); } } @@ -384,7 +384,7 @@ class RequestHandlingTest_FormActionController extends Controller { return 'formactionInAllowedActions'; } - public function getViewer(){ + public function getViewer($action = null) { return new SSViewer('BlankPage'); } diff --git a/tests/forms/FormScaffolderTest.php b/tests/forms/FormScaffolderTest.php index 365d9d0ac..e95874866 100644 --- a/tests/forms/FormScaffolderTest.php +++ b/tests/forms/FormScaffolderTest.php @@ -128,11 +128,13 @@ class FormScaffolderTest_ArticleExtension extends DataExtension implements TestO static $db = array( 'ExtendedField' => 'Varchar' ); - function updateCMSFields(&$fields) { + + function updateCMSFields(FieldList $fields) { $fields->addFieldToTab('Root.Main', new TextField('AddedExtensionField') ); } + } DataObject::add_extension('FormScaffolderTest_Article', 'FormScaffolderTest_ArticleExtension'); diff --git a/tests/forms/FormTest.php b/tests/forms/FormTest.php index 7ad8565fb..d6e0b5fa7 100644 --- a/tests/forms/FormTest.php +++ b/tests/forms/FormTest.php @@ -468,7 +468,7 @@ class FormTest_Controller extends Controller implements TestOnly { return $this->redirectBack(); } - function getViewer(){ + function getViewer($action = null) { return new SSViewer('BlankPage'); } @@ -505,7 +505,7 @@ class FormTest_ControllerWithSecurityToken extends Controller implements TestOnl return $this->redirectBack(); } - function getViewer(){ + function getViewer($action = null) { return new SSViewer('BlankPage'); } } diff --git a/tests/forms/gridfield/GridFieldDetailFormTest.php b/tests/forms/gridfield/GridFieldDetailFormTest.php index c66a9fe9e..d71655811 100644 --- a/tests/forms/gridfield/GridFieldDetailFormTest.php +++ b/tests/forms/gridfield/GridFieldDetailFormTest.php @@ -140,8 +140,8 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly { 'Categories' => 'GridFieldDetailFormTest_Category' ); - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('Categories', Object::create('GridField', 'Categories', 'Categories', @@ -162,8 +162,8 @@ class GridFieldDetailFormTest_PeopleGroup extends DataObject implements TestOnly 'People' => 'GridFieldDetailFormTest_Person' ); - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('People', Object::create('GridField', 'People', 'People', @@ -184,8 +184,8 @@ class GridFieldDetailFormTest_Category extends DataObject implements TestOnly { 'People' => 'GridFieldDetailFormTest_Person' ); - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('People', Object::create('GridField', 'People', 'People', diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php index d25231373..0e6cff31b 100644 --- a/tests/forms/uploadfield/UploadFieldTest.php +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -567,21 +567,21 @@ static $many_many = array( class UploadFieldTest_FileExtension extends DataExtension implements TestOnly { - function extraStatics() { + function extraStatics($class = null, $extension = null) { return array( 'has_one' => array('Record' => 'UploadFieldTest_Record') ); } - function canDelete() { + function canDelete($member = null) { if($this->owner->Name == 'nodelete.txt') return false; } - function canEdit() { + function canEdit($member = null) { if($this->owner->Name == 'noedit.txt') return false; } - function canView() { + function canView($member = null) { if($this->owner->Name == 'noview.txt') return false; } } @@ -655,4 +655,4 @@ class UploadFieldTest_Controller extends Controller implements TestOnly { } -} \ No newline at end of file +} diff --git a/tests/model/AggregateTest.php b/tests/model/AggregateTest.php index 615b59361..2c1dc15bc 100644 --- a/tests/model/AggregateTest.php +++ b/tests/model/AggregateTest.php @@ -73,13 +73,15 @@ class AggregateTest extends SapphireTest { * Test basic aggregation on a passed type */ function testTypeSpecifiedAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + // Template style access - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->XML_val('Max', array('Foo')), 9); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->XML_val('Max', array('Fab')), 3); + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->XML_val('Max', array('Foo')), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->XML_val('Max', array('Fab')), 3); // PHP style access - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->Max('Foo'), 9); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->Max('Fab'), 3); + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->Max('Fab'), 3); } /* */ @@ -90,7 +92,7 @@ class AggregateTest extends SapphireTest { function testAutoTypeAggregate() { $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); $fab = $this->objFromFixture('AggregateTest_Fab', 'fab1'); - + // Template style access $this->assertEquals($foo->Aggregate()->XML_val('Max', array('Foo')), 9); $this->assertEquals($fab->Aggregate()->XML_val('Max', array('Fab')), 3); @@ -106,13 +108,15 @@ class AggregateTest extends SapphireTest { * @return unknown_type */ function testBaseFieldAggregate() { + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + $this->assertEquals( - $this->formatDate(DataObject::Aggregate('AggregateTest_Foo')->Max('LastEdited')), + $this->formatDate($foo->Aggregate('AggregateTest_Foo')->Max('LastEdited')), $this->formatDate(DataObject::get_one('AggregateTest_Foo', '', '', '"LastEdited" DESC')->LastEdited) ); $this->assertEquals( - $this->formatDate(DataObject::Aggregate('AggregateTest_Foo')->Max('Created')), + $this->formatDate($foo->Aggregate('AggregateTest_Foo')->Max('Created')), $this->formatDate(DataObject::get_one('AggregateTest_Foo', '', '', '"Created" DESC')->Created) ); } @@ -122,13 +126,14 @@ class AggregateTest extends SapphireTest { * Test aggregation takes place on the passed type & it's children only */ function testChildAggregate() { - + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + // For base classes, aggregate is calculcated on it and all children classes - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); // For subclasses, aggregate is calculated for that subclass and it's children only - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->Max('Foo'), 9); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + $this->assertEquals($foo->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($foo->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); } /* */ @@ -145,35 +150,35 @@ class AggregateTest extends SapphireTest { * Test cache is correctly flushed on write */ function testCacheFlushing() { - + $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + $fab = $this->objFromFixture('AggregateTest_Fab', 'fab1'); + // For base classes, aggregate is calculcated on it and all children classes - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 9); // For subclasses, aggregate is calculated for that subclass and it's children only - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->Max('Foo'), 9); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fac')->Max('Foo'), 6); - - $foo = $this->objFromFixture('AggregateTest_Foo', 'foo1'); + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + $foo->Foo = 12; $foo->write(); // For base classes, aggregate is calculcated on it and all children classes - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->Max('Foo'), 12); + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 12); // For subclasses, aggregate is calculated for that subclass and it's children only - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->Max('Foo'), 9); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 9); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); - $fab = $this->objFromFixture('AggregateTest_Fab', 'fab1'); $fab->Foo = 15; $fab->write(); // For base classes, aggregate is calculcated on it and all children classes - $this->assertEquals(DataObject::Aggregate('AggregateTest_Foo')->Max('Foo'), 15); + $this->assertEquals($fab->Aggregate('AggregateTest_Foo')->Max('Foo'), 15); // For subclasses, aggregate is calculated for that subclass and it's children only - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fab')->Max('Foo'), 15); - $this->assertEquals(DataObject::Aggregate('AggregateTest_Fac')->Max('Foo'), 6); + $this->assertEquals($fab->Aggregate('AggregateTest_Fab')->Max('Foo'), 15); + $this->assertEquals($fab->Aggregate('AggregateTest_Fac')->Max('Foo'), 6); } /* */ diff --git a/tests/model/DataDifferencerTest.php b/tests/model/DataDifferencerTest.php index c2cee4898..443173fc9 100644 --- a/tests/model/DataDifferencerTest.php +++ b/tests/model/DataDifferencerTest.php @@ -62,8 +62,8 @@ class DataDifferencerTest_Object extends DataObject implements TestOnly { 'HasOneRelation' => 'DataDifferencerTest_HasOneRelationObject' ); - function getCMSFields() { - $fields = parent::getCMSFields(); + function getCMSFields($params = null) { + $fields = parent::getCMSFields($params); $choices = array( 'a' => 'a', 'b' => 'b', @@ -103,4 +103,4 @@ class DataDifferencerTest_MockImage extends Image implements TestOnly { // Skip aktual generation return $gd; } -} \ No newline at end of file +} diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index b3e0c08ec..9e970926a 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -132,7 +132,7 @@ class GroupTest extends FunctionalTest { class GroupTest_Member extends Member implements TestOnly { - function getCMSFields() { + function getCMSFields($params = null) { $groups = DataObject::get('Group'); $groupsMap = ($groups) ? $groups->map() : false; $fields = new FieldList( diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php index b485aecd0..5e8656760 100644 --- a/tests/security/MemberTest.php +++ b/tests/security/MemberTest.php @@ -591,29 +591,29 @@ class MemberTest extends FunctionalTest { } class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly { - public function canView() { + public function canView($member = null) { return true; } } class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly { - public function canView() { + public function canView($member = null) { return false; } } class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly { - public function canView() { + public function canView($member = null) { return true; } - public function canEdit() { + public function canEdit($member = null) { return true; } - public function canDelete() { + public function canDelete($member = null) { return false; } diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index c68d6d219..df0b6ecba 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -930,7 +930,7 @@ class SSViewerTestFixture extends ViewableData { } - function XML_val($fieldName, $arguments = null) { + function XML_val($fieldName, $arguments = null, $cache = false) { if(preg_match('/NotSet/i', $fieldName)) { return ''; } else if(preg_match('/Raw/i', $fieldName)) { @@ -940,7 +940,7 @@ class SSViewerTestFixture extends ViewableData { } } - function hasValue($fieldName, $arguments = null) { + function hasValue($fieldName, $arguments = null, $cache = true) { return (bool)$this->XML_val($fieldName, $arguments); } } @@ -1021,4 +1021,4 @@ class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly { return 'z' . implode(':', $args) . 'z'; } -} \ No newline at end of file +} From bd95bcaf6149b04ce4478d257098b469646f0f7d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 26 Mar 2012 22:05:38 +1300 Subject: [PATCH 08/44] BUGFIX Nested Group records should be removed, along with the parent. --- security/Group.php | 18 +++++++++++------- tests/security/GroupTest.php | 22 +++++++++++++--------- tests/security/GroupTest.yml | 5 ++++- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/security/Group.php b/security/Group.php index b4b8fde83..6290016f1 100755 --- a/security/Group.php +++ b/security/Group.php @@ -337,17 +337,21 @@ class Group extends DataObject { if(!$this->Code) $this->setCode($this->Title); } } - - function onAfterDelete() { - parent::onAfterDelete(); - + + function onBeforeDelete() { + parent::onBeforeDelete(); + + // if deleting this group, delete it's children as well + foreach($this->Groups() as $group) { + $group->delete(); + } + // Delete associated permissions - $permissions = $this->Permissions(); - foreach ( $permissions as $permission ) { + foreach($this->Permissions() as $permission) { $permission->delete(); } } - + /** * Checks for permission-code CMS_ACCESS_SecurityAdmin. * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well. diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index b3e0c08ec..2d364168d 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -94,15 +94,6 @@ class GroupTest extends FunctionalTest { } - function testDelete() { - $adminGroup = $this->objFromFixture('Group', 'admingroup'); - - $adminGroup->delete(); - - $this->assertEquals(0, DataObject::get('Group', "\"ID\"={$adminGroup->ID}")->count(), 'Group is removed'); - $this->assertEquals(0, DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}")->count(), 'Permissions removed along with the group'); - } - function testCollateAncestorIDs() { $parentGroup = $this->objFromFixture('Group', 'parentgroup'); $childGroup = $this->objFromFixture('Group', 'childgroup'); @@ -128,6 +119,19 @@ class GroupTest extends FunctionalTest { 'Orphaned nodes dont contain invalid parent IDs' ); } + + public function testDelete() { + $group = $this->objFromFixture('Group', 'parentgroup'); + $groupID = $group->ID; + $childGroupID = $this->idFromFixture('Group', 'childgroup'); + $group->delete(); + + $this->assertEquals(0, DataObject::get('Group', "\"ID\" = {$groupID}")->Count(), 'Group is removed'); + $this->assertEquals(0, DataObject::get('Permission', "\"GroupID\" = {$groupID}")->Count(), 'Permissions removed along with the group'); + $this->assertEquals(0, DataObject::get('Group', "\"ParentID\" = {$groupID}")->Count(), 'Child groups are removed'); + $this->assertEquals(0, DataObject::get('Group', "\"ParentID\" = {$childGroupID}")->Count(), 'Grandchild groups are removed'); + } + } class GroupTest_Member extends Member implements TestOnly { diff --git a/tests/security/GroupTest.yml b/tests/security/GroupTest.yml index a0c3d8a97..13ca9f5ce 100644 --- a/tests/security/GroupTest.yml +++ b/tests/security/GroupTest.yml @@ -6,6 +6,9 @@ Group: childgroup: Code: childgroup Parent: =>Group.parentgroup + grandchildgroup: + Code: grandchildgroup + Parent: =>Group.childgroup group1: Title: Group 1 group2: @@ -26,4 +29,4 @@ GroupTest_Member: Permission: admincode: Code: ADMIN - Group: =>Group.admingroup \ No newline at end of file + Group: =>Group.admingroup From c56176c5d576b4e1c9b5739cf711fd437f5cc756 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 28 Mar 2012 22:55:26 +0200 Subject: [PATCH 09/44] MINOR Updated 'from-source' installation to use new 'simple' theme (which also fixes the problem of checking out 'blackcandy' sub themes via git into overlapping repository paths) --- docs/en/installation/from-source.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/en/installation/from-source.md b/docs/en/installation/from-source.md index 645cc8712..0d3941409 100644 --- a/docs/en/installation/from-source.md +++ b/docs/en/installation/from-source.md @@ -26,7 +26,7 @@ SilverStripe core is currently hosted on [github.com/silverstripe](http://github * The `installer` project ([github.com/silverstripe/silverstripe-installer](http://github.com/silverstripe/silverstripe-installer)) * The `sapphire` module ([github.com/silverstripe/sapphire](http://github.com/silverstripe/sapphire)) * The `cms` module ([github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms)) - * A sample theme called `blackcandy` ([github.com/silverstripe-themes/silverstripe-blackcandy](http://github.com/silverstripe-themes/silverstripe-blackcandy)) + * A sample theme called `simple` ([github.com/silverstripe-themes/silverstripe-simple](http://github.com/silverstripe-themes/silverstripe-simple)) First, you'll have to decide what you want to do with your project: @@ -72,7 +72,7 @@ Run the following command to download all core dependencies via [Piston](http:// cd my-silverstripe-project/ tools/new-project -This will add `sapphire`, `cms` and the `blackcandy` theme to your project. +This will add `sapphire`, `cms` and the `simple` theme to your project. As a fallback solution, you can simply download all necessary files without any dependency management through piston. This is handy if you have an existing project in version control, and want a one-off snapshot @@ -146,8 +146,7 @@ Please replace `` below with your github username. cd my-silverstripe-project git clone git@github.com:/sapphire.git sapphire git clone git@github.com:/silverstripe-cms.git cms - rm -rf themes - git clone git@github.com:/silverstripe-blackcandy.git themes + git clone git@github.com:/silverstripe-simple.git themes/simple Now you need to add the original repository as `upstream`, so you can keep your fork updated later on. @@ -155,7 +154,7 @@ Now you need to add the original repository as `upstream`, so you can keep your (git remote add upstream git://github.com/silverstripe/silverstripe-installer.git && git fetch upstream) (cd sapphire && git remote add upstream git://github.com/silverstripe/sapphire.git && git fetch upstream) (cd cms && git remote add upstream git://github.com/silverstripe/silverstripe-cms.git && git fetch upstream) - (cd themes/blackcandy && git remote add upstream git://github.com/silverstripe-themes/silverstripe-blackcandy.git) + (cd themes/simple && git remote add upstream git://github.com/silverstripe-themes/silverstripe-simple.git) Now that you're set up, please read our ["Collaboration on Git"](../misc/collaboration-on-git) guide, as well as our general ["Contributor guidelines"](../misc/contributing). @@ -175,7 +174,7 @@ You can optionally select a ["release branch"](https://github.com/silverstripe/s git checkout -b 2.4 origin/2.4 (cd sapphire && git checkout -b 2.4 origin/2.4) (cd cms && git checkout -b 2.4 origin/2.4) - (cd themes/blackcandy && git checkout -b 2.4 origin/2.4) + (cd themes/simple && git checkout -b 2.4 origin/2.4) # repeat for all modules in your project... After creating the local branch, you can simply switch between branches: @@ -184,7 +183,7 @@ After creating the local branch, you can simply switch between branches: git checkout 2.4 (cd sapphire && git checkout 2.4) (cd cms && git checkout 2.4) - (cd themes/blackcandy && git checkout 2.4) + (cd themes/simple && git checkout 2.4) # repeat for all modules in your project... To switch back to master: @@ -193,7 +192,7 @@ To switch back to master: git checkout master (cd sapphire && git checkout master) (cd cms && git checkout master) - (cd themes/blackcandy && git checkout master) + (cd themes/simple && git checkout master) # repeat for all modules in your project... You can't switch branches if your working copy has local changes (typically in `mysite/_config.php`). From 58433d38ced3d255d2704271cc80a7f163cad910 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Fri, 30 Mar 2012 15:59:47 +1300 Subject: [PATCH 10/44] BUGFIX: Tidied up relObject() behaviour on DataLists to restore broken SearchContext functionality. --- model/DataList.php | 4 ++++ model/DataObject.php | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/model/DataList.php b/model/DataList.php index 931419c25..721cedd50 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -668,6 +668,10 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab return singleton($this->dataClass)->$relationName()->forForeignID($ids); } + function dbObject($fieldName) { + return singleton($this->dataClass)->dbObject($fieldName); + } + /** * Add a number of items to the component set. * diff --git a/model/DataObject.php b/model/DataObject.php index 4a5add52a..024a01e15 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -2404,7 +2404,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Traverse dot syntax $component = $this; foreach($parts as $relation) { - $component = $component->$relation(); + if($component instanceof SS_List) { + if(method_exists($component,$relation)) $component = $component->$relation(); + else $component = $component->relation($relation); + } else { + $component = $component->$relation(); + } } $object = $component->dbObject($fieldName); @@ -2436,7 +2441,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Traverse dot syntax $component = $this; foreach($parts as $relation) { - $component = $component->$relation(); + if($component instanceof SS_List) { + if(method_exists($component,$relation)) $component = $component->$relation(); + else $component = $component->relation($relation); + } else { + $component = $component->$relation(); + } } return $component->$fieldName; From c518a19ec2e41609cfcc6f218c3e39f876e07943 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:22:40 +0200 Subject: [PATCH 11/44] BUGFIX Replaced logic for checking external URLs in CMS Menu with more stable jQuery Mobile codebase (fixes problems on IE not loading menu entries via ajax) (#7002) --- admin/javascript/LeftAndMain.Content.js | 6 +++--- admin/javascript/LeftAndMain.Menu.js | 10 +++------- admin/javascript/lib.js | 4 ---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index 2852be827..cbc9dc726 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -159,10 +159,10 @@ self.submitForm_responseHandler(form, xmlhttp.responseText, status, xmlhttp, formData); } - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it + // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. if(window.History.enabled) { var url = xmlhttp.getResponseHeader('X-ControllerURL'); - if(url) window.history.replaceState({}, '', url); + if(url) window.History.replaceState({}, '', url); } // Re-init tabs (in case the form tag itself is a tabset) @@ -287,7 +287,7 @@ var url = $(node).find('a:first').attr('href'); if(url && url != '#') { - if($(node).find('a:first').is(':internal')) url = url = $.path.makeUrlAbsolute(url, $('base').attr('href')); + if($.path.isExternal($(node).find('a:first'))) url = url = $.path.makeUrlAbsolute(url, $('base').attr('href')); // Reload only edit form if it exists (side-by-side view of tree and edit view), otherwise reload whole panel if(container.find('.cms-edit-form').length) { url += '?cms-view-form=1'; diff --git a/admin/javascript/LeftAndMain.Menu.js b/admin/javascript/LeftAndMain.Menu.js index aeb8226f1..7f6792d63 100644 --- a/admin/javascript/LeftAndMain.Menu.js +++ b/admin/javascript/LeftAndMain.Menu.js @@ -207,16 +207,16 @@ onclick: function(e) { // Only catch left clicks, in order to allow opening in tabs. // Ignore external links, fallback to standard link behaviour - if(e.which > 1 || this.is(':external')) return; + var isExternal = $.path.isExternal(this.attr('href')); + if(e.which > 1 || isExternal) return; e.preventDefault(); var item = this.getMenuItem(); var url = this.attr('href'); - if(this.is(':internal')) url = $('base').attr('href') + url; + if(!isExternal) url = $('base').attr('href') + url; var children = item.find('li'); - if(children.length) { children.first().find('a').click(); } else { @@ -261,8 +261,4 @@ }); }); - - // Internal Helper - $.expr[':'].internal = function(obj){return obj.href.match(/^mailto\:/) || (obj.hostname == location.hostname);}; - $.expr[':'].external = function(obj){return !$(obj).is(':internal');}; }(jQuery)); \ No newline at end of file diff --git a/admin/javascript/lib.js b/admin/javascript/lib.js index 6df032e0c..c6437cc7e 100644 --- a/admin/javascript/lib.js +++ b/admin/javascript/lib.js @@ -232,8 +232,4 @@ }; $.path = path; - - // Internal Helper - $.expr[':'].internal = function(obj){return obj.href.match(/^mailto\:/) || (obj.hostname == location.hostname);}; - $.expr[':'].external = function(obj){return !$(obj).is(':internal')}; }(jQuery)); \ No newline at end of file From 3f4aba55454f5c27486e208a9517ed1c65ead2a9 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:23:39 +0200 Subject: [PATCH 12/44] MINOR Avoid breaking IE on CMS ajax responses which don't contain CSS class names (splitting on NULL) (#7002) --- admin/javascript/LeftAndMain.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index b8c77f4f0..a7a502a3d 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -223,12 +223,13 @@ jQuery.noConflict(); var layoutClasses = ['east', 'west', 'center', 'north', 'south']; var elemClasses = contentEl.attr('class'); - var origLayoutClasses = $.grep( - elemClasses.split(' '), - function(val) { - return ($.inArray(val, layoutClasses) >= 0); - } - ); + var origLayoutClasses = ''; + if(elemClasses) { + origLayoutClasses = $.grep( + elemClasses.split(' '), + function(val) { return ($.inArray(val, layoutClasses) >= 0);} + ); + } newContentEl .removeClass(layoutClasses.join(' ')) From f34e58f5739fc936e62f95b89246b46d7981aafb Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:24:36 +0200 Subject: [PATCH 13/44] ENHANCEMENT Enabled History.pushState() support in IE via onhashchange fallbacks (#7002) --- admin/code/LeftAndMain.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 5993c9318..df88b41a3 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -250,7 +250,7 @@ class LeftAndMain extends Controller implements PermissionProvider { SAPPHIRE_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.adapter.jquery.js', - // SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js', + SAPPHIRE_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js', THIRDPARTY_DIR . '/jstree/jquery.jstree.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js', SAPPHIRE_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js', From e6aa9ae017c57b4f223061254e4b1dcd1c84b778 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:25:31 +0200 Subject: [PATCH 14/44] BUGFIX Fixed History.js library handling of relative URLs combined with a base URL (was causing infinite loops, e.g. /admin/#/admin/admin/admin/security) (#7002) --- .../history-js/scripts/uncompressed/history.html4.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/admin/thirdparty/history-js/scripts/uncompressed/history.html4.js b/admin/thirdparty/history-js/scripts/uncompressed/history.html4.js index 6d8d4274a..709d9b887 100644 --- a/admin/thirdparty/history-js/scripts/uncompressed/history.html4.js +++ b/admin/thirdparty/history-js/scripts/uncompressed/history.html4.js @@ -429,7 +429,10 @@ } // Create State - currentState = History.extractState(History.getFullUrl(currentHash||document.location.href,false),true); + // MODIFIED ischommer: URL normalization needs to respect our tag, + // otherwise will go into infinite loops + currentState = History.extractState(History.getFullUrl(currentHash||document.location.href,true),true); + // END MODIFIED // Check if we are the same state if ( History.isLastSavedState(currentState) ) { From 83adffd7cd951c4cad6a640fdb74fcae96723dba Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:26:04 +0200 Subject: [PATCH 15/44] MINOR Fixed var names in LeftAndMain.js --- admin/javascript/LeftAndMain.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index a7a502a3d..219efc360 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -253,7 +253,7 @@ jQuery.noConflict(); // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it if(window.History.enabled) { var url = xhr.getResponseHeader('X-ControllerURL'); - if(url) window.history.replaceState({}, '', url); + if(url) window.History.replaceState({}, '', url); } self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: newContentEl}); From 1091c7b9441e289d7f76a0d138ecaba9d0231c07 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:57:54 +0200 Subject: [PATCH 16/44] BUGFIX Don't replace pushState() if emulated, as it will re-load the new URL via ajax, effectively duplicating every request in IE (#7002) --- admin/javascript/LeftAndMain.Content.js | 3 ++- admin/javascript/LeftAndMain.js | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index cbc9dc726..a0e514c31 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -160,7 +160,8 @@ } // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. - if(window.History.enabled) { + // Causes non-pushState browser to re-request the URL, so ignore for those. + if(window.History.enabled && !History.emulated.pushState) { var url = xmlhttp.getResponseHeader('X-ControllerURL'); if(url) window.History.replaceState({}, '', url); } diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 219efc360..3cef834bf 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -88,7 +88,8 @@ jQuery.noConflict(); $('.cms-edit-form').live('reloadeditform', function(e, data) { // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it - if(window.History.enabled) { + // Causes non-pushState browser to re-request the URL, so ignore for those. + if(window.History.enabled && !History.emulated.pushState) { var url = data.xmlhttp.getResponseHeader('X-ControllerURL'); if(url) window.history.replaceState({}, '', url); } @@ -250,12 +251,13 @@ jQuery.noConflict(); newContentEl.css('visibility', 'visible'); newContentEl.removeClass('loading'); - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it - if(window.History.enabled) { + // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. + // Causes non-pushState browser to re-request the URL, so ignore for those. + if(window.History.enabled && !History.emulated.pushState) { var url = xhr.getResponseHeader('X-ControllerURL'); if(url) window.History.replaceState({}, '', url); } - + self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: newContentEl}); }, error: function(xhr, status, e) { From 6c91aa0ec5a215343934c5b059c1e6174aa388e0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 15:58:54 +0200 Subject: [PATCH 17/44] BUGFIX Force referer via "BackURL" POST data in CMS to work around IE problems with sending the base URL as the referer instead of the actual one (#7002) --- admin/javascript/LeftAndMain.Content.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index a0e514c31..d1cf3d221 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -142,6 +142,11 @@ var formData = form.serializeArray(); // add button action formData.push({name: $(button).attr('name'), value:'1'}); + // Artificial HTTP referer, IE doesn't submit them via ajax. + // Also rewrites anchors to their page counterparts, which is important + // as automatic browser ajax response redirects seem to discard the hash/fragment. + formData.push({name: 'BackURL', value:History.getPageUrl()}); + jQuery.ajax(jQuery.extend({ url: form.attr('action'), data: formData, From 7c1b40d4a72e2553a34f78013a053104dcd17537 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 16:55:35 +0200 Subject: [PATCH 18/44] MINOR Added 'updateCMSFields' hook to File (fixes #7091) --- filesystem/File.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/filesystem/File.php b/filesystem/File.php index 4a951b0cb..8d93a0a7b 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -316,7 +316,10 @@ class File extends DataObject { } /** - * Returns the fields to power the edit screen of files in the CMS + * Returns the fields to power the edit screen of files in the CMS. + * You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension} + * and implemeting updateCMSFields(FieldList $fields) on that extension. + * * @return FieldList */ function getCMSFields($params = null) { @@ -383,6 +386,9 @@ class File extends DataObject { ) ); + // Folder has its own updateCMSFields hook + if(!($this instanceof Folder)) $this->extend('updateCMSFields', $fields); + return $fields; } From 18a1cc1db3646e73fb110ff92e1c81249b020f79 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Sat, 31 Mar 2012 09:03:54 +1300 Subject: [PATCH 19/44] MINOR: update docs to fix issues raised via comments. MINOR: remove section on comments. --- docs/en/tutorials/2-extending-a-basic-site.md | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/docs/en/tutorials/2-extending-a-basic-site.md b/docs/en/tutorials/2-extending-a-basic-site.md index 198d8f523..e5662a23f 100644 --- a/docs/en/tutorials/2-extending-a-basic-site.md +++ b/docs/en/tutorials/2-extending-a-basic-site.md @@ -87,7 +87,6 @@ We'll start with the *ArticlePage* page type. First we create the model, a class } - ?> Here we've created our data object/controller pair, but we haven't actually extended them at all. Don't worry about the @@ -116,8 +115,6 @@ Let's create the *ArticleHolder* page type. class ArticleHolder_Controller extends Page_Controller { } - - ?> Here we have done something interesting: the *$allowed_children* field. This is one of a number of static fields we can @@ -249,7 +246,7 @@ Let's walk through these changes. *$dateField* is added only to the DateField in order to change the configuration. :::php - $dateField->setConfig('showCalendar', true); + $dateField->setConfig('showcalendar', true); Set *showCalendar* to true to have a calendar appear underneath the Date field when you click on the field. @@ -424,44 +421,6 @@ This will change the icons for the pages in the CMS. ![](_images/icons2.jpg) -### Allowing comments on news articles - -A handy feature built into SilverStripe is the ability for guests to your site to leave comments on pages. We can turn -this on for an article simply by ticking the box in the behaviour tab of a page in the CMS. Enable this for all your -*ArticlePage*s. - -![](_images/comments.jpg) - -We then need to include *$PageComments* in our template, which will insert the comment form as well as all comments left -on the page. - -**themes/tutorial/templates/Layout/ArticlePage.ss** - - :::html - ... -
- Posted on $Date.Nice by $Author -
- $PageComments - ... - - -You should also prepare the *Page* template in the same manner, so comments can be enabled at a later point on any page. - -![](_images/news-comments.jpg) - -It would be nice to have comments on for all articles by default. We can do this with the *$defaults* array. Add this to -the *ArticlePage* class: - - :::php - static $defaults = array( - 'ProvideComments' => true - ); - - -You can set defaults for any of the fields in your data object. *ProvideComments* is defined in *SiteTree*, so it is -part of our *ArticlePage* data object. - ## Showing the latest news on the homepage It would be nice to greet page visitors with a summary of the latest news when they visit the homepage. This requires a From 8ae474b18252f7b5ebccc6c626adce99be8d01e7 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 30 Mar 2012 16:18:14 +1300 Subject: [PATCH 20/44] API CHANGE Remove use of Services_JSON and replace with json_encode() and json_decode() API CHANGE Convert::json2array() will convert nested JSON structures to array as well for easier traversal, instead of array with nested objects. --- core/Convert.php | 76 +++++-------------- tests/core/ConvertTest.php | 3 +- .../GridFieldAddExistingAutocompleterTest.php | 4 +- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/core/Convert.php b/core/Convert.php index ac13d72ae..61a79fbbe 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -92,28 +92,28 @@ class Convert { return str_replace(array("\\", '"', "\n", "\r", "'"), array("\\\\", '\"', '\n', '\r', "\\'"), $val); } } - + /** - * Uses the PHP 5.2 native json_encode function if available, - * otherwise falls back to the Services_JSON class. - * - * @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 - * @uses Director::baseFolder() - * @uses Services_JSON + * Encode a value as a JSON encoded string. * - * @param mixed $val - * @return string JSON safe string + * @param mixed $val Value to be encoded + * @return string JSON encoded string */ static function raw2json($val) { - if(function_exists('json_encode')) { - return json_encode($val); - } else { - require_once(Director::baseFolder() . '/sapphire/thirdparty/json/JSON.php'); - $json = new Services_JSON(); - return $json->encode($val); - } + return json_encode($val); } - + + /** + * Encode an array as a JSON encoded string. + * THis is an alias to {@link raw2json()} + * + * @param array $val Array to convert + * @return string JSON encoded string + */ + static function array2json($val) { + return self::raw2json($val); + } + static function raw2sql($val) { if(is_array($val)) { foreach($val as $k => $v) $val[$k] = self::raw2sql($v); @@ -138,41 +138,15 @@ class Convert { else return html_entity_decode($val, ENT_QUOTES, 'UTF-8'); } } - - /** - * Convert an array into a JSON encoded string. - * - * @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 - * @uses Director::baseFolder() - * @uses Services_JSON - * - * @param array $val Array to convert - * @return string JSON encoded string - */ - static function array2json($val) { - if(function_exists('json_encode')) { - return json_encode($val); - } else { - require_once(Director::baseFolder() . '/sapphire/thirdparty/json/JSON.php'); - $json = new Services_JSON(); - return $json->encode($val); - } - } - + /** * Convert a JSON encoded string into an object. - * - * @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 - * @uses Director::baseFolder() - * @uses Services_JSON * * @param string $val - * @return mixed JSON safe string + * @return mixed */ static function json2obj($val) { - require_once(Director::baseFolder() . '/sapphire/thirdparty/json/JSON.php'); - $json = new Services_JSON(); - return $json->decode($val); + return json_decode($val); } /** @@ -183,15 +157,7 @@ class Convert { * @return array|boolean */ static function json2array($val) { - $json = self::json2obj($val); - if(!$json) return false; - - $arr = array(); - foreach($json as $k => $v) { - $arr[$k] = $v; - } - - return $arr; + return json_decode($val, true); } /** diff --git a/tests/core/ConvertTest.php b/tests/core/ConvertTest.php index b94b79cc7..5e82eec0f 100644 --- a/tests/core/ConvertTest.php +++ b/tests/core/ConvertTest.php @@ -101,6 +101,7 @@ class ConvertTest extends SapphireTest { $this->assertEquals(3, count($decoded), '3 items in the decoded array'); $this->assertContains('Bloggs', $decoded, 'Contains "Bloggs" value in decoded array'); $this->assertContains('Jones', $decoded, 'Contains "Jones" value in decoded array'); + $this->assertContains('Structure', $decoded['My']['Complicated']); } function testJSON2Obj() { @@ -121,4 +122,4 @@ class ConvertTest extends SapphireTest { $this->assertEquals('foos-bar-2', Convert::raw2url('foo\'s [bar] (2)')); } -} \ No newline at end of file +} diff --git a/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php b/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php index af7397865..0d0eecd30 100644 --- a/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php +++ b/tests/forms/gridfield/GridFieldAddExistingAutocompleterTest.php @@ -33,7 +33,7 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest { ); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); - $this->assertFalse($result); + $this->assertEmpty($result, 'The output is either an empty array or boolean FALSE'); } function testAdd() { @@ -80,4 +80,4 @@ class GridFieldAddExistingAutocompleterTest_Controller extends Controller implem $field = new GridField('testfield', 'testfield', $player->Teams(), $config); return new Form($this, 'Form', new FieldList($field), new FieldList()); } -} \ No newline at end of file +} From dbc4be3e94aad6dc0097ba46640278cfb75f4bf4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 30 Mar 2012 16:23:55 +1300 Subject: [PATCH 21/44] API CHANGE Removed Services_JSON library, as we use the built-in json functions instead. --- core/Convert.php | 2 +- thirdparty/json/.piston.yml | 8 - thirdparty/json/JSON.php | 806 ---------------------------------- thirdparty/json/LICENSE | 21 - thirdparty/json/Test-JSON.php | 521 ---------------------- 5 files changed, 1 insertion(+), 1357 deletions(-) delete mode 100644 thirdparty/json/.piston.yml delete mode 100644 thirdparty/json/JSON.php delete mode 100644 thirdparty/json/LICENSE delete mode 100644 thirdparty/json/Test-JSON.php diff --git a/core/Convert.php b/core/Convert.php index 61a79fbbe..f4a8d9635 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -143,7 +143,7 @@ class Convert { * Convert a JSON encoded string into an object. * * @param string $val - * @return mixed + * @return object|boolean */ static function json2obj($val) { return json_decode($val); diff --git a/thirdparty/json/.piston.yml b/thirdparty/json/.piston.yml deleted file mode 100644 index 11765e63a..000000000 --- a/thirdparty/json/.piston.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -format: 1 -handler: - piston:remote-revision: 93520 - piston:uuid: 467b73ca-7a2a-4603-9d3b-597d59a354a9 -lock: false -repository_class: Piston::Svn::Repository -repository_url: http://svn.silverstripe.com/open/thirdparty/json/tags/1.31 diff --git a/thirdparty/json/JSON.php b/thirdparty/json/JSON.php deleted file mode 100644 index 0cddbddb4..000000000 --- a/thirdparty/json/JSON.php +++ /dev/null @@ -1,806 +0,0 @@ - - * @author Matt Knapp - * @author Brett Stimmerman - * @copyright 2005 Michal Migurski - * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ - * @license http://www.opensource.org/licenses/bsd-license.php - * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 - */ - -/** - * Marker constant for Services_JSON::decode(), used to flag stack state - */ -define('SERVICES_JSON_SLICE', 1); - -/** - * Marker constant for Services_JSON::decode(), used to flag stack state - */ -define('SERVICES_JSON_IN_STR', 2); - -/** - * Marker constant for Services_JSON::decode(), used to flag stack state - */ -define('SERVICES_JSON_IN_ARR', 3); - -/** - * Marker constant for Services_JSON::decode(), used to flag stack state - */ -define('SERVICES_JSON_IN_OBJ', 4); - -/** - * Marker constant for Services_JSON::decode(), used to flag stack state - */ -define('SERVICES_JSON_IN_CMT', 5); - -/** - * Behavior switch for Services_JSON::decode() - */ -define('SERVICES_JSON_LOOSE_TYPE', 16); - -/** - * Behavior switch for Services_JSON::decode() - */ -define('SERVICES_JSON_SUPPRESS_ERRORS', 32); - -/** - * Converts to and from JSON format. - * - * Brief example of use: - * - * - * // create a new instance of Services_JSON - * $json = new Services_JSON(); - * - * // convert a complexe value to JSON notation, and send it to the browser - * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); - * $output = $json->encode($value); - * - * print($output); - * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] - * - * // accept incoming POST data, assumed to be in JSON notation - * $input = file_get_contents('php://input', 1000000); - * $value = $json->decode($input); - * - */ -class Services_JSON -{ - /** - * constructs a new JSON instance - * - * @param int $use object behavior flags; combine with boolean-OR - * - * possible values: - * - SERVICES_JSON_LOOSE_TYPE: loose typing. - * "{...}" syntax creates associative arrays - * instead of objects in decode(). - * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. - * Values which can't be encoded (e.g. resources) - * appear as NULL instead of throwing errors. - * By default, a deeply-nested resource will - * bubble up with an error, so all return values - * from encode() should be checked with isError() - */ - function Services_JSON($use = 0) - { - $this->use = $use; - } - - /** - * convert a string from one UTF-16 char to one UTF-8 char - * - * Normally should be handled by mb_convert_encoding, but - * provides a slower PHP-only method for installations - * that lack the multibye string extension. - * - * @param string $utf16 UTF-16 character - * @return string UTF-8 character - * @access private - */ - function utf162utf8($utf16) - { - // oh please oh please oh please oh please oh please - if(function_exists('mb_convert_encoding')) { - return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); - } - - $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); - - switch(true) { - case ((0x7F & $bytes) == $bytes): - // this case should never be reached, because we are in ASCII range - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0x7F & $bytes); - - case (0x07FF & $bytes) == $bytes: - // return a 2-byte UTF-8 character - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0xC0 | (($bytes >> 6) & 0x1F)) - . chr(0x80 | ($bytes & 0x3F)); - - case (0xFFFF & $bytes) == $bytes: - // return a 3-byte UTF-8 character - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0xE0 | (($bytes >> 12) & 0x0F)) - . chr(0x80 | (($bytes >> 6) & 0x3F)) - . chr(0x80 | ($bytes & 0x3F)); - } - - // ignoring UTF-32 for now, sorry - return ''; - } - - /** - * convert a string from one UTF-8 char to one UTF-16 char - * - * Normally should be handled by mb_convert_encoding, but - * provides a slower PHP-only method for installations - * that lack the multibye string extension. - * - * @param string $utf8 UTF-8 character - * @return string UTF-16 character - * @access private - */ - function utf82utf16($utf8) - { - // oh please oh please oh please oh please oh please - if(function_exists('mb_convert_encoding')) { - return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); - } - - switch(strlen($utf8)) { - case 1: - // this case should never be reached, because we are in ASCII range - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return $utf8; - - case 2: - // return a UTF-16 character from a 2-byte UTF-8 char - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0x07 & (ord($utf8{0}) >> 2)) - . chr((0xC0 & (ord($utf8{0}) << 6)) - | (0x3F & ord($utf8{1}))); - - case 3: - // return a UTF-16 character from a 3-byte UTF-8 char - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr((0xF0 & (ord($utf8{0}) << 4)) - | (0x0F & (ord($utf8{1}) >> 2))) - . chr((0xC0 & (ord($utf8{1}) << 6)) - | (0x7F & ord($utf8{2}))); - } - - // ignoring UTF-32 for now, sorry - return ''; - } - - /** - * encodes an arbitrary variable into JSON format - * - * @param mixed $var any number, boolean, string, array, or object to be encoded. - * see argument 1 to Services_JSON() above for array-parsing behavior. - * if var is a strng, note that encode() always expects it - * to be in ASCII or UTF-8 format! - * - * @return mixed JSON string representation of input var or an error if a problem occurs - * @access public - */ - function encode($var) - { - switch (gettype($var)) { - case 'boolean': - return $var ? 'true' : 'false'; - - case 'NULL': - return 'null'; - - case 'integer': - return (int) $var; - - case 'double': - case 'float': - return (float) $var; - - case 'string': - // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT - $ascii = ''; - $strlen_var = strlen($var); - - /* - * Iterate over every character in the string, - * escaping with a slash or encoding to UTF-8 where necessary - */ - for ($c = 0; $c < $strlen_var; ++$c) { - - $ord_var_c = ord($var{$c}); - - switch (true) { - case $ord_var_c == 0x08: - $ascii .= '\b'; - break; - case $ord_var_c == 0x09: - $ascii .= '\t'; - break; - case $ord_var_c == 0x0A: - $ascii .= '\n'; - break; - case $ord_var_c == 0x0C: - $ascii .= '\f'; - break; - case $ord_var_c == 0x0D: - $ascii .= '\r'; - break; - - case $ord_var_c == 0x22: - case $ord_var_c == 0x2F: - case $ord_var_c == 0x5C: - // double quote, slash, slosh - $ascii .= '\\'.$var{$c}; - break; - - case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): - // characters U-00000000 - U-0000007F (same as ASCII) - $ascii .= $var{$c}; - break; - - case (($ord_var_c & 0xE0) == 0xC0): - // characters U-00000080 - U-000007FF, mask 110XXXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, ord($var{$c + 1})); - $c += 1; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xF0) == 0xE0): - // characters U-00000800 - U-0000FFFF, mask 1110XXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2})); - $c += 2; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xF8) == 0xF0): - // characters U-00010000 - U-001FFFFF, mask 11110XXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3})); - $c += 3; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xFC) == 0xF8): - // characters U-00200000 - U-03FFFFFF, mask 111110XX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3}), - ord($var{$c + 4})); - $c += 4; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xFE) == 0xFC): - // characters U-04000000 - U-7FFFFFFF, mask 1111110X - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3}), - ord($var{$c + 4}), - ord($var{$c + 5})); - $c += 5; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - } - } - - return '"'.$ascii.'"'; - - case 'array': - /* - * As per JSON spec if any array key is not an integer - * we must treat the the whole array as an object. We - * also try to catch a sparsely populated associative - * array with numeric keys here because some JS engines - * will create an array with empty indexes up to - * max_index which can cause memory issues and because - * the keys, which may be relevant, will be remapped - * otherwise. - * - * As per the ECMA and JSON specification an object may - * have any string as a property. Unfortunately due to - * a hole in the ECMA specification if the key is a - * ECMA reserved word or starts with a digit the - * parameter is only accessible using ECMAScript's - * bracket notation. - */ - - // treat as a JSON object - if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { - $properties = array_map(array($this, 'name_value'), - array_keys($var), - array_values($var)); - - foreach($properties as $property) { - if(Services_JSON::isError($property)) { - return $property; - } - } - - return '{' . join(',', $properties) . '}'; - } - - // treat it like a regular array - $elements = array_map(array($this, 'encode'), $var); - - foreach($elements as $element) { - if(Services_JSON::isError($element)) { - return $element; - } - } - - return '[' . join(',', $elements) . ']'; - - case 'object': - $vars = get_object_vars($var); - - $properties = array_map(array($this, 'name_value'), - array_keys($vars), - array_values($vars)); - - foreach($properties as $property) { - if(Services_JSON::isError($property)) { - return $property; - } - } - - return '{' . join(',', $properties) . '}'; - - default: - return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) - ? 'null' - : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); - } - } - - /** - * array-walking function for use in generating JSON-formatted name-value pairs - * - * @param string $name name of key to use - * @param mixed $value reference to an array element to be encoded - * - * @return string JSON-formatted name-value pair, like '"name":value' - * @access private - */ - function name_value($name, $value) - { - $encoded_value = $this->encode($value); - - if(Services_JSON::isError($encoded_value)) { - return $encoded_value; - } - - return $this->encode(strval($name)) . ':' . $encoded_value; - } - - /** - * reduce a string by removing leading and trailing comments and whitespace - * - * @param $str string string value to strip of comments and whitespace - * - * @return string string value stripped of comments and whitespace - * @access private - */ - function reduce_string($str) - { - $str = preg_replace(array( - - // eliminate single line comments in '// ...' form - '#^\s*//(.+)$#m', - - // eliminate multi-line comments in '/* ... */' form, at start of string - '#^\s*/\*(.+)\*/#Us', - - // eliminate multi-line comments in '/* ... */' form, at end of string - '#/\*(.+)\*/\s*$#Us' - - ), '', $str); - - // eliminate extraneous space - return trim($str); - } - - /** - * decodes a JSON string into appropriate variable - * - * @param string $str JSON-formatted string - * - * @return mixed number, boolean, string, array, or object - * corresponding to given JSON input string. - * See argument 1 to Services_JSON() above for object-output behavior. - * Note that decode() always returns strings - * in ASCII or UTF-8 format! - * @access public - */ - function decode($str) - { - $str = $this->reduce_string($str); - - switch (strtolower($str)) { - case 'true': - return true; - - case 'false': - return false; - - case 'null': - return null; - - default: - $m = array(); - - if (is_numeric($str)) { - // Lookie-loo, it's a number - - // This would work on its own, but I'm trying to be - // good about returning integers where appropriate: - // return (float)$str; - - // Return float or int, as appropriate - return ((float)$str == (integer)$str) - ? (integer)$str - : (float)$str; - - } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { - // STRINGS RETURNED IN UTF-8 FORMAT - $delim = substr($str, 0, 1); - $chrs = substr($str, 1, -1); - $utf8 = ''; - $strlen_chrs = strlen($chrs); - - for ($c = 0; $c < $strlen_chrs; ++$c) { - - $substr_chrs_c_2 = substr($chrs, $c, 2); - $ord_chrs_c = ord($chrs{$c}); - - switch (true) { - case $substr_chrs_c_2 == '\b': - $utf8 .= chr(0x08); - ++$c; - break; - case $substr_chrs_c_2 == '\t': - $utf8 .= chr(0x09); - ++$c; - break; - case $substr_chrs_c_2 == '\n': - $utf8 .= chr(0x0A); - ++$c; - break; - case $substr_chrs_c_2 == '\f': - $utf8 .= chr(0x0C); - ++$c; - break; - case $substr_chrs_c_2 == '\r': - $utf8 .= chr(0x0D); - ++$c; - break; - - case $substr_chrs_c_2 == '\\"': - case $substr_chrs_c_2 == '\\\'': - case $substr_chrs_c_2 == '\\\\': - case $substr_chrs_c_2 == '\\/': - if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || - ($delim == "'" && $substr_chrs_c_2 != '\\"')) { - $utf8 .= $chrs{++$c}; - } - break; - - case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): - // single, escaped unicode character - $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) - . chr(hexdec(substr($chrs, ($c + 4), 2))); - $utf8 .= $this->utf162utf8($utf16); - $c += 5; - break; - - case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): - $utf8 .= $chrs{$c}; - break; - - case ($ord_chrs_c & 0xE0) == 0xC0: - // characters U-00000080 - U-000007FF, mask 110XXXXX - //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 2); - ++$c; - break; - - case ($ord_chrs_c & 0xF0) == 0xE0: - // characters U-00000800 - U-0000FFFF, mask 1110XXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 3); - $c += 2; - break; - - case ($ord_chrs_c & 0xF8) == 0xF0: - // characters U-00010000 - U-001FFFFF, mask 11110XXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 4); - $c += 3; - break; - - case ($ord_chrs_c & 0xFC) == 0xF8: - // characters U-00200000 - U-03FFFFFF, mask 111110XX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 5); - $c += 4; - break; - - case ($ord_chrs_c & 0xFE) == 0xFC: - // characters U-04000000 - U-7FFFFFFF, mask 1111110X - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 6); - $c += 5; - break; - - } - - } - - return $utf8; - - } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { - // array, or object notation - - if ($str{0} == '[') { - $stk = array(SERVICES_JSON_IN_ARR); - $arr = array(); - } else { - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $stk = array(SERVICES_JSON_IN_OBJ); - $obj = array(); - } else { - $stk = array(SERVICES_JSON_IN_OBJ); - $obj = new stdClass(); - } - } - - array_push($stk, array('what' => SERVICES_JSON_SLICE, - 'where' => 0, - 'delim' => false)); - - $chrs = substr($str, 1, -1); - $chrs = $this->reduce_string($chrs); - - if ($chrs == '') { - if (reset($stk) == SERVICES_JSON_IN_ARR) { - return $arr; - - } else { - return $obj; - - } - } - - //print("\nparsing {$chrs}\n"); - - $strlen_chrs = strlen($chrs); - - for ($c = 0; $c <= $strlen_chrs; ++$c) { - - $top = end($stk); - $substr_chrs_c_2 = substr($chrs, $c, 2); - - if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { - // found a comma that is not inside a string, array, etc., - // OR we've reached the end of the character list - $slice = substr($chrs, $top['where'], ($c - $top['where'])); - array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); - //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - if (reset($stk) == SERVICES_JSON_IN_ARR) { - // we are in an array, so just push an element onto the stack - array_push($arr, $this->decode($slice)); - - } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { - // we are in an object, so figure - // out the property name and set an - // element in an associative array, - // for now - $parts = array(); - - if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { - // "name":value pair - $key = $this->decode($parts[1]); - $val = $this->decode($parts[2]); - - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $obj[$key] = $val; - } else { - $obj->$key = $val; - } - } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { - // name:value pair, where name is unquoted - $key = $parts[1]; - $val = $this->decode($parts[2]); - - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $obj[$key] = $val; - } else { - $obj->$key = $val; - } - } - - } - - } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { - // found a quote, and we are not inside a string - array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); - //print("Found start of string at {$c}\n"); - - } elseif (($chrs{$c} == $top['delim']) && - ($top['what'] == SERVICES_JSON_IN_STR) && - ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { - // found a quote, we're in a string, and it's not escaped - // we know that it's not escaped becase there is _not_ an - // odd number of backslashes at the end of the string so far - array_pop($stk); - //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); - - } elseif (($chrs{$c} == '[') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a left-bracket, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); - //print("Found start of array at {$c}\n"); - - } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { - // found a right-bracket, and we're in an array - array_pop($stk); - //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } elseif (($chrs{$c} == '{') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a left-brace, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); - //print("Found start of object at {$c}\n"); - - } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { - // found a right-brace, and we're in an object - array_pop($stk); - //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } elseif (($substr_chrs_c_2 == '/*') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a comment start, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); - $c++; - //print("Found start of comment at {$c}\n"); - - } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { - // found a comment end, and we're in one now - array_pop($stk); - $c++; - - for ($i = $top['where']; $i <= $c; ++$i) - $chrs = substr_replace($chrs, ' ', $i, 1); - - //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } - - } - - if (reset($stk) == SERVICES_JSON_IN_ARR) { - return $arr; - - } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { - return $obj; - - } - - } - } - } - - /** - * @todo Ultimately, this should just call PEAR::isError() - */ - function isError($data, $code = null) - { - if (class_exists('pear')) { - return PEAR::isError($data, $code); - } elseif (is_object($data) && (get_class($data) == 'services_json_error' || - is_subclass_of($data, 'services_json_error'))) { - return true; - } - - return false; - } -} - -if (class_exists('PEAR_Error')) { - - class Services_JSON_Error extends PEAR_Error - { - function Services_JSON_Error($message = 'unknown error', $code = null, - $mode = null, $options = null, $userinfo = null) - { - parent::PEAR_Error($message, $code, $mode, $options, $userinfo); - } - } - -} else { - - /** - * @todo Ultimately, this class shall be descended from PEAR_Error - */ - class Services_JSON_Error - { - function Services_JSON_Error($message = 'unknown error', $code = null, - $mode = null, $options = null, $userinfo = null) - { - - } - } - -} - -?> diff --git a/thirdparty/json/LICENSE b/thirdparty/json/LICENSE deleted file mode 100644 index 4ae6bef55..000000000 --- a/thirdparty/json/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN -NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/thirdparty/json/Test-JSON.php b/thirdparty/json/Test-JSON.php deleted file mode 100644 index 4a437f743..000000000 --- a/thirdparty/json/Test-JSON.php +++ /dev/null @@ -1,521 +0,0 @@ - - * @author Matt Knapp - * @author Brett Stimmerman - * @copyright 2005 Michal Migurski - * @version CVS: $Id: Test-JSON.php,v 1.28 2006/06/28 05:54:17 migurski Exp $ - * @license http://www.opensource.org/licenses/bsd-license.php - * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 - */ - - error_reporting(E_ALL); - - require_once 'PHPUnit.php'; - require_once 'JSON.php'; - - class Services_JSON_EncDec_TestCase extends PHPUnit_TestCase { - - function Services_JSON_EncDec_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json = new Services_JSON(); - - $obj = new stdClass(); - $obj->a_string = '"he":llo}:{world'; - $obj->an_array = array(1, 2, 3); - $obj->obj = new stdClass(); - $obj->obj->a_number = 123; - - $this->obj = $obj; - $this->obj_j = '{"a_string":"\"he\":llo}:{world","an_array":[1,2,3],"obj":{"a_number":123}}'; - $this->obj_d = 'object with properties, nested object and arrays'; - - $this->arr = array(null, true, array(1, 2, 3), "hello\"],[world!"); - $this->arr_j = '[null,true,[1,2,3],"hello\"],[world!"]'; - $this->arr_d = 'array with elements and nested arrays'; - - $this->str1 = 'hello world'; - $this->str1_j = '"hello world"'; - $this->str1_j_ = "'hello world'"; - $this->str1_d = 'hello world'; - $this->str1_d_ = 'hello world, double quotes'; - - $this->str2 = "hello\t\"world\""; - $this->str2_j = '"hello\\t\\"world\\""'; - $this->str2_d = 'hello world, with tab, double-quotes'; - - $this->str3 = "\\\r\n\t\"/"; - $this->str3_j = '"\\\\\\r\\n\\t\\"\\/"'; - $this->str3_d = 'backslash, return, newline, tab, double-quote'; - - $this->str4 = 'héllö wørłd'; - $this->str4_j = '"h\u00e9ll\u00f6 w\u00f8r\u0142d"'; - $this->str4_j_ = '"héllö wørłd"'; - $this->str4_d = 'hello world, with unicode'; - } - - function test_to_JSON() - { - $this->assertEquals('null', $this->json->encode(null), 'type case: null'); - $this->assertEquals('true', $this->json->encode(true), 'type case: boolean true'); - $this->assertEquals('false', $this->json->encode(false), 'type case: boolean false'); - - $this->assertEquals('1', $this->json->encode(1), 'numeric case: 1'); - $this->assertEquals('-1', $this->json->encode(-1), 'numeric case: -1'); - $this->assertEquals('1.000000', $this->json->encode(1.0), 'numeric case: 1.0'); - $this->assertEquals('1.100000', $this->json->encode(1.1), 'numeric case: 1.1'); - - $this->assertEquals($this->str1_j, $this->json->encode($this->str1), "string case: {$this->str1_d}"); - $this->assertEquals($this->str2_j, $this->json->encode($this->str2), "string case: {$this->str2_d}"); - $this->assertEquals($this->str3_j, $this->json->encode($this->str3), "string case: {$this->str3_d}"); - $this->assertEquals($this->str4_j, $this->json->encode($this->str4), "string case: {$this->str4_d}"); - - $this->assertEquals($this->arr_j, $this->json->encode($this->arr), "array case: {$this->arr_d}"); - $this->assertEquals($this->obj_j, $this->json->encode($this->obj), "object case: {$this->obj_d}"); - } - - function test_from_JSON() - { - $this->assertEquals(null, $this->json->decode('null'), 'type case: null'); - $this->assertEquals(true, $this->json->decode('true'), 'type case: boolean true'); - $this->assertEquals(false, $this->json->decode('false'), 'type case: boolean false'); - - $this->assertEquals(1, $this->json->decode('1'), 'numeric case: 1'); - $this->assertEquals(-1, $this->json->decode('-1'), 'numeric case: -1'); - $this->assertEquals(1.0, $this->json->decode('1.0'), 'numeric case: 1.0'); - $this->assertEquals(1.1, $this->json->decode('1.1'), 'numeric case: 1.1'); - - $this->assertEquals(11.0, $this->json->decode('1.1e1'), 'numeric case: 1.1e1'); - $this->assertEquals(11.0, $this->json->decode('1.10e+1'), 'numeric case: 1.10e+1'); - $this->assertEquals(0.11, $this->json->decode('1.1e-1'), 'numeric case: 1.1e-1'); - $this->assertEquals(-0.11, $this->json->decode('-1.1e-1'), 'numeric case: -1.1e-1'); - - $this->assertEquals($this->str1, $this->json->decode($this->str1_j), "string case: {$this->str1_d}"); - $this->assertEquals($this->str1, $this->json->decode($this->str1_j_), "string case: {$this->str1_d_}"); - $this->assertEquals($this->str2, $this->json->decode($this->str2_j), "string case: {$this->str2_d}"); - $this->assertEquals($this->str3, $this->json->decode($this->str3_j), "string case: {$this->str3_d}"); - $this->assertEquals($this->str4, $this->json->decode($this->str4_j), "string case: {$this->str4_d}"); - $this->assertEquals($this->str4, $this->json->decode($this->str4_j_), "string case: {$this->str4_d}"); - - $this->assertEquals($this->arr, $this->json->decode($this->arr_j), "array case: {$this->arr_d}"); - $this->assertEquals($this->obj, $this->json->decode($this->obj_j), "object case: {$this->obj_d}"); - } - - function test_to_then_from_JSON() - { - $this->assertEquals(null, $this->json->decode($this->json->encode(null)), 'type case: null'); - $this->assertEquals(true, $this->json->decode($this->json->encode(true)), 'type case: boolean true'); - $this->assertEquals(false, $this->json->decode($this->json->encode(false)), 'type case: boolean false'); - - $this->assertEquals(1, $this->json->decode($this->json->encode(1)), 'numeric case: 1'); - $this->assertEquals(-1, $this->json->decode($this->json->encode(-1)), 'numeric case: -1'); - $this->assertEquals(1.0, $this->json->decode($this->json->encode(1.0)), 'numeric case: 1.0'); - $this->assertEquals(1.1, $this->json->decode($this->json->encode(1.1)), 'numeric case: 1.1'); - - $this->assertEquals($this->str1, $this->json->decode($this->json->encode($this->str1)), "string case: {$this->str1_d}"); - $this->assertEquals($this->str2, $this->json->decode($this->json->encode($this->str2)), "string case: {$this->str2_d}"); - $this->assertEquals($this->str3, $this->json->decode($this->json->encode($this->str3)), "string case: {$this->str3_d}"); - $this->assertEquals($this->str4, $this->json->decode($this->json->encode($this->str4)), "string case: {$this->str4_d}"); - - $this->assertEquals($this->arr, $this->json->decode($this->json->encode($this->arr)), "array case: {$this->arr_d}"); - $this->assertEquals($this->obj, $this->json->decode($this->json->encode($this->obj)), "object case: {$this->obj_d}"); - } - - function test_from_then_to_JSON() - { - $this->assertEquals('null', $this->json->encode($this->json->decode('null')), 'type case: null'); - $this->assertEquals('true', $this->json->encode($this->json->decode('true')), 'type case: boolean true'); - $this->assertEquals('false', $this->json->encode($this->json->decode('false')), 'type case: boolean false'); - - $this->assertEquals('1', $this->json->encode($this->json->decode('1')), 'numeric case: 1'); - $this->assertEquals('-1', $this->json->encode($this->json->decode('-1')), 'numeric case: -1'); - $this->assertEquals('1.0', $this->json->encode($this->json->decode('1.0')), 'numeric case: 1.0'); - $this->assertEquals('1.1', $this->json->encode($this->json->decode('1.1')), 'numeric case: 1.1'); - - $this->assertEquals($this->str1_j, $this->json->encode($this->json->decode($this->str1_j)), "string case: {$this->str1_d}"); - $this->assertEquals($this->str2_j, $this->json->encode($this->json->decode($this->str2_j)), "string case: {$this->str2_d}"); - $this->assertEquals($this->str3_j, $this->json->encode($this->json->decode($this->str3_j)), "string case: {$this->str3_d}"); - $this->assertEquals($this->str4_j, $this->json->encode($this->json->decode($this->str4_j)), "string case: {$this->str4_d}"); - $this->assertEquals($this->str4_j, $this->json->encode($this->json->decode($this->str4_j_)), "string case: {$this->str4_d}"); - - $this->assertEquals($this->arr_j, $this->json->encode($this->json->decode($this->arr_j)), "array case: {$this->arr_d}"); - $this->assertEquals($this->obj_j, $this->json->encode($this->json->decode($this->obj_j)), "object case: {$this->obj_d}"); - } - } - - class Services_JSON_AssocArray_TestCase extends PHPUnit_TestCase { - - function Services_JSON_AssocArray_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json_l = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - $this->json_s = new Services_JSON(); - - $this->arr = array('car1'=> array('color'=> 'tan', 'model' => 'sedan'), - 'car2' => array('color' => 'red', 'model' => 'sports')); - $this->arr_jo = '{"car1":{"color":"tan","model":"sedan"},"car2":{"color":"red","model":"sports"}}'; - $this->arr_d = 'associative array with nested associative arrays'; - - $this->arn = array(0=> array(0=> 'tan\\', 'model\\' => 'sedan'), 1 => array(0 => 'red', 'model' => 'sports')); - $this->arn_ja = '[{"0":"tan\\\\","model\\\\":"sedan"},{"0":"red","model":"sports"}]'; - $this->arn_d = 'associative array with nested associative arrays, and some numeric keys thrown in'; - - $this->arrs = array (1 => 'one', 2 => 'two', 5 => 'five'); - $this->arrs_jo = '{"1":"one","2":"two","5":"five"}'; - $this->arrs_d = 'associative array numeric keys which are not fully populated in a range of 0 to length-1'; - } - - function test_type() - { - $this->assertEquals('array', gettype($this->json_l->decode($this->arn_ja)), "loose type should be array"); - $this->assertEquals('array', gettype($this->json_s->decode($this->arn_ja)), "strict type should be array"); - } - - function test_to_JSON() - { - // both strict and loose JSON should result in an object - $this->assertEquals($this->arr_jo, $this->json_l->encode($this->arr), "array case - loose: {$this->arr_d}"); - $this->assertEquals($this->arr_jo, $this->json_s->encode($this->arr), "array case - strict: {$this->arr_d}"); - - // ...unless the input array has some numeric indeces, in which case the behavior is to degrade to a regular array - $this->assertEquals($this->arn_ja, $this->json_s->encode($this->arn), "array case - strict: {$this->arn_d}"); - - // Test a sparsely populated numerically indexed associative array - $this->assertEquals($this->arrs_jo, $this->json_l->encode($this->arrs), "sparse numeric assoc array: {$this->arrs_d}"); - } - - function test_to_then_from_JSON() - { - // these tests motivated by a bug in which strings that end - // with backslashes followed by quotes were incorrectly decoded. - - foreach(array('\\"', '\\\\"', '\\"\\"', '\\""\\""', '\\\\"\\\\"') as $v) { - $this->assertEquals(array($v), $this->json_l->decode($this->json_l->encode(array($v)))); - $this->assertEquals(array('a' => $v), $this->json_l->decode($this->json_l->encode(array('a' => $v)))); - } - } - } - - class Services_JSON_NestedArray_TestCase extends PHPUnit_TestCase { - - function Services_JSON_NestedArray_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - - $this->str1 = '[{"this":"that"}]'; - $this->arr1 = array(array('this' => 'that')); - - $this->str2 = '{"this":["that"]}'; - $this->arr2 = array('this' => array('that')); - - $this->str3 = '{"params":[{"foo":["1"],"bar":"1"}]}'; - $this->arr3 = array('params' => array(array('foo' => array('1'), 'bar' => '1'))); - - $this->str4 = '{"0": {"foo": "bar", "baz": "winkle"}}'; - $this->arr4 = array('0' => array('foo' => 'bar', 'baz' => 'winkle')); - - $this->str5 = '{"params":[{"options": {"old": [ ], "new": {"0": {"elements": {"old": [], "new": {"0": {"elementName": "aa", "isDefault": false, "elementRank": "0", "priceAdjust": "0", "partNumber": ""}}}, "optionName": "aa", "isRequired": false, "optionDesc": null}}}}]}'; - $this->arr5 = array ( - 'params' => array ( - 0 => array ( - 'options' => - array ( - 'old' => array(), - 'new' => array ( - 0 => array ( - 'elements' => array ( - 'old' => array(), - 'new' => array ( - 0 => array ( - 'elementName' => 'aa', - 'isDefault' => false, - 'elementRank' => '0', - 'priceAdjust' => '0', - 'partNumber' => '', - ), - ), - ), - 'optionName' => 'aa', - 'isRequired' => false, - 'optionDesc' => NULL, - ), - ), - ), - ), - ), - ); - } - - function test_type() - { - $this->assertEquals('array', gettype($this->json->decode($this->str1)), "loose type should be array"); - $this->assertEquals('array', gettype($this->json->decode($this->str2)), "loose type should be array"); - $this->assertEquals('array', gettype($this->json->decode($this->str3)), "loose type should be array"); - } - - function test_from_JSON() - { - $this->assertEquals($this->arr1, $this->json->decode($this->str1), "simple compactly-nested array"); - $this->assertEquals($this->arr2, $this->json->decode($this->str2), "simple compactly-nested array"); - $this->assertEquals($this->arr3, $this->json->decode($this->str3), "complex compactly nested array"); - $this->assertEquals($this->arr4, $this->json->decode($this->str4), "complex compactly nested array"); - $this->assertEquals($this->arr5, $this->json->decode($this->str5), "super complex compactly nested array"); - } - - function _test_from_JSON() - { - $super = '{"params":[{"options": {"old": {}, "new": {"0": {"elements": {"old": {}, "new": {"0": {"elementName": "aa", "isDefault": false, "elementRank": "0", "priceAdjust": "0", "partNumber": ""}}}, "optionName": "aa", "isRequired": false, "optionDesc": ""}}}}]}'; - print("trying {$super}...\n"); - print var_export($this->json->decode($super)); - } - } - - class Services_JSON_Object_TestCase extends PHPUnit_TestCase { - - function Services_JSON_Object_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json_l = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - $this->json_s = new Services_JSON(); - - $this->obj_j = '{"a_string":"\"he\":llo}:{world","an_array":[1,2,3],"obj":{"a_number":123}}'; - - $this->obj1->car1->color = 'tan'; - $this->obj1->car1->model = 'sedan'; - $this->obj1->car2->color = 'red'; - $this->obj1->car2->model = 'sports'; - $this->obj1_j = '{"car1":{"color":"tan","model":"sedan"},"car2":{"color":"red","model":"sports"}}'; - $this->obj1_d = 'Object with nested objects'; - } - - function test_type() - { - $this->assertEquals('object', gettype($this->json_s->decode($this->obj_j)), "checking whether decoded type is object"); - $this->assertEquals('array', gettype($this->json_l->decode($this->obj_j)), "checking whether decoded type is array"); - } - - function test_to_JSON() - { - $this->assertEquals($this->obj1_j, $this->json_s->encode($this->obj1), "object - strict: {$this->obj1_d}"); - $this->assertEquals($this->obj1_j, $this->json_l->encode($this->obj1), "object - loose: {$this->obj1_d}"); - } - - function test_from_then_to_JSON() - { - $this->assertEquals($this->obj_j, $this->json_s->encode($this->json_s->decode($this->obj_j)), "object case"); - $this->assertEquals($this->obj_j, $this->json_l->encode($this->json_l->decode($this->obj_j)), "array case"); - } - } - - class Services_JSON_Spaces_Comments_TestCase extends PHPUnit_TestCase { - - function Services_JSON_Spaces_Comments_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - - $this->obj_j = '{"a_string":"\"he\":llo}:{world","an_array":[1,2,3],"obj":{"a_number":123}}'; - - $this->obj_js = '{"a_string": "\"he\":llo}:{world", - "an_array":[1, 2, 3], - "obj": {"a_number":123}}'; - - $this->obj_jc1 = '{"a_string": "\"he\":llo}:{world", - // here is a comment, hoorah - "an_array":[1, 2, 3], - "obj": {"a_number":123}}'; - - $this->obj_jc2 = '/* this here is the sneetch */ "the sneetch" - // this has been the sneetch.'; - - $this->obj_jc3 = '{"a_string": "\"he\":llo}:{world", - /* here is a comment, hoorah */ - "an_array":[1, 2, 3 /* and here is another */], - "obj": {"a_number":123}}'; - - $this->obj_jc4 = '{\'a_string\': "\"he\":llo}:{world", - /* here is a comment, hoorah */ - \'an_array\':[1, 2, 3 /* and here is another */], - "obj": {"a_number":123}}'; - } - - function test_spaces() - { - $this->assertEquals($this->json->decode($this->obj_j), $this->json->decode($this->obj_js), "checking whether notation with spaces works"); - } - - function test_comments() - { - $this->assertEquals($this->json->decode($this->obj_j), $this->json->decode($this->obj_jc1), "checking whether notation with single line comments works"); - $this->assertEquals('the sneetch', $this->json->decode($this->obj_jc2), "checking whether notation with multiline comments works"); - $this->assertEquals($this->json->decode($this->obj_j), $this->json->decode($this->obj_jc3), "checking whether notation with multiline comments works"); - $this->assertEquals($this->json->decode($this->obj_j), $this->json->decode($this->obj_jc4), "checking whether notation with single-quotes and multiline comments works"); - } - } - - class Services_JSON_Empties_TestCase extends PHPUnit_TestCase { - - function Services_JSON_Empties_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json_l = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - $this->json_s = new Services_JSON(); - - $this->obj0_j = '{}'; - $this->arr0_j = '[]'; - - $this->obj1_j = '{ }'; - $this->arr1_j = '[ ]'; - - $this->obj2_j = '{ /* comment inside */ }'; - $this->arr2_j = '[ /* comment inside */ ]'; - } - - function test_type() - { - $this->assertEquals('array', gettype($this->json_l->decode($this->arr0_j)), "should be array"); - $this->assertEquals('object', gettype($this->json_s->decode($this->obj0_j)), "should be object"); - - $this->assertEquals(0, count($this->json_l->decode($this->arr0_j)), "should be empty array"); - $this->assertEquals(0, count(get_object_vars($this->json_s->decode($this->obj0_j))), "should be empty object"); - - $this->assertEquals('array', gettype($this->json_l->decode($this->arr1_j)), "should be array, even with space"); - $this->assertEquals('object', gettype($this->json_s->decode($this->obj1_j)), "should be object, even with space"); - - $this->assertEquals(0, count($this->json_l->decode($this->arr1_j)), "should be empty array, even with space"); - $this->assertEquals(0, count(get_object_vars($this->json_s->decode($this->obj1_j))), "should be empty object, even with space"); - - $this->assertEquals('array', gettype($this->json_l->decode($this->arr2_j)), "should be array, despite comment"); - $this->assertEquals('object', gettype($this->json_s->decode($this->obj2_j)), "should be object, despite comment"); - - $this->assertEquals(0, count($this->json_l->decode($this->arr2_j)), "should be empty array, despite comment"); - $this->assertEquals(0, count(get_object_vars($this->json_s->decode($this->obj2_j))), "should be empty object, despite commentt"); - } - } - - class Services_JSON_UnquotedKeys_TestCase extends PHPUnit_TestCase { - - function Services_JSON_UnquotedKeys_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); - - $this->arn = array(0=> array(0=> 'tan', 'model' => 'sedan'), 1 => array(0 => 'red', 'model' => 'sports')); - $this->arn_ja = '[{0:"tan","model":"sedan"},{"0":"red",model:"sports"}]'; - $this->arn_d = 'associative array with unquoted keys, nested associative arrays, and some numeric keys thrown in'; - - $this->arrs = array (1 => 'one', 2 => 'two', 5 => 'fi"ve'); - $this->arrs_jo = '{"1":"one",2:"two","5":\'fi"ve\'}'; - $this->arrs_d = 'associative array with unquoted keys, single-quoted values, numeric keys which are not fully populated in a range of 0 to length-1'; - } - - function test_from_JSON() - { - // ...unless the input array has some numeric indeces, in which case the behavior is to degrade to a regular array - $this->assertEquals($this->arn, $this->json->decode($this->arn_ja), "array case - strict: {$this->arn_d}"); - - // Test a sparsely populated numerically indexed associative array - $this->assertEquals($this->arrs, $this->json->decode($this->arrs_jo), "sparse numeric assoc array: {$this->arrs_d}"); - } - } - - class Services_JSON_ErrorSuppression_TestCase extends PHPUnit_TestCase { - - function Services_JSON_ErrorSuppression_TestCase($name) { - $this->PHPUnit_TestCase($name); - } - - function setUp() { - $this->json = new Services_JSON(); - $this->json_ = new Services_JSON(SERVICES_JSON_SUPPRESS_ERRORS); - - $this->res = tmpfile(); - $this->res_j_ = 'null'; - $this->res_d = 'naked resource'; - - $this->arr = array('a', 1, tmpfile()); - $this->arr_j_ = '["a",1,null]'; - $this->arr_d = 'array with string, number and resource'; - - $obj = new stdClass(); - $obj->a_string = '"he":llo}:{world'; - $obj->an_array = array(1, 2, 3); - $obj->resource = tmpfile(); - - $this->obj = $obj; - $this->obj_j_ = '{"a_string":"\"he\":llo}:{world","an_array":[1,2,3],"resource":null}'; - $this->obj_d = 'object with properties, array, and nested resource'; - } - - function test_to_JSON() - { - $this->assertTrue(Services_JSON::isError($this->json->encode($this->res)), "resource case: {$this->res_d}"); - $this->assertTrue(Services_JSON::isError($this->json->encode($this->arr)), "array case: {$this->arr_d}"); - $this->assertTrue(Services_JSON::isError($this->json->encode($this->obj)), "object case: {$this->obj_d}"); - } - - function test_to_JSON_suppressed() - { - $this->assertEquals($this->res_j_, $this->json_->encode($this->res), "resource case: {$this->res_d}"); - $this->assertEquals($this->arr_j_, $this->json_->encode($this->arr), "array case: {$this->arr_d}"); - $this->assertEquals($this->obj_j_, $this->json_->encode($this->obj), "object case: {$this->obj_d}"); - } - } - - $suite = new PHPUnit_TestSuite('Services_JSON_EncDec_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_AssocArray_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_NestedArray_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_Object_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_Spaces_Comments_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_Empties_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_UnquotedKeys_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - - $suite = new PHPUnit_TestSuite('Services_JSON_ErrorSuppression_TestCase'); - $result = PHPUnit::run($suite); - echo $result->toString(); - -?> From c52f75e3194fd309e6ac05ed957e3da8e8f44ab4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 31 Mar 2012 13:33:05 +1300 Subject: [PATCH 22/44] MINOR Add notes on upgrading for Convert::json2array() changes --- docs/en/changelogs/3.0.0.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index bc84a8bea..04cd56b29 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/docs/en/changelogs/3.0.0.md @@ -113,6 +113,38 @@ As with any SilverStripe upgrade, we recommend database backups before calling ` See [mysql.com](http://dev.mysql.com/doc/refman/5.5/en/converting-tables-to-innodb.html) for details on the conversion. Note: MySQL has made InnoDB the default engine in its [5.5 release](http://dev.mysql.com/doc/refman/5.5/en/innodb-storage-engine.html). +### Convert::json2array() changes ### + +Convert JSON functions have been changed to use built-in json PHP functions `json_decode()` and `json_encode()` + +Because `json_decode()` will convert nested JSON structures to arrays as well, this has changed the way it worked, +as before nested structures would be converted to an object instead. + +So, given the following JSON input to `Convert::json2array()`: + + {"Joe":"Bloggs","Tom":"Jones","My":{"Complicated":"Structure"}} + +Here's the output from SilverStripe 2.4, with nested JSON as objects: + + array( + 'Joe' => 'Bloggs' + 'Tom' => 'Jones', + 'My' => stdObject( + Complicated => 'Structure' // property on object + ) + ) + +Now in SilverStripe 3.x, nested structures are arrays: + + array( + 'Joe' => 'Bloggs', + 'Tom' => 'Jones', + 'My' => array( + 'Complicated' => 'Structure' // key value on nested array + ) + ) + + ### GridField: Replacement for TableListField and ComplexTableField ### We have a new component for managing lists of objects: The `[GridField](/topics/grid-field)`. From 89267419d47f4819eb777c5170cd899a3273f511 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 2 Apr 2012 14:10:20 +1200 Subject: [PATCH 23/44] BUGFIX When inserting an image in HtmlEditorField, don't append "px" as the width and height attributes only accept a number without a unit --- javascript/HtmlEditorField.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/HtmlEditorField.js b/javascript/HtmlEditorField.js index 16b3bd770..e6852b574 100644 --- a/javascript/HtmlEditorField.js +++ b/javascript/HtmlEditorField.js @@ -782,8 +782,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; return { 'src' : this.find(':input[name=URL]').val(), 'alt' : this.find(':input[name=AltText]').val(), - 'width' : width ? parseInt(width, 10) + "px" : null, - 'height' : height ? parseInt(height, 10) + "px" : null, + 'width' : width ? parseInt(width, 10) : null, + 'height' : height ? parseInt(height, 10) : null, 'title' : this.find(':input[name=Title]').val(), 'class' : this.find(':input[name=CSSClass]').val() }; From a2979f0551832e7b33b7ea6be9c96ce9bec3387c Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 2 Apr 2012 14:29:02 +1200 Subject: [PATCH 24/44] BUGFIX Ensure that origLayoutClasses is always an array when considered empty, as join() will be called later and causes a JS error on an empty string. --- admin/javascript/LeftAndMain.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 3cef834bf..ffdd578e6 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -224,7 +224,7 @@ jQuery.noConflict(); var layoutClasses = ['east', 'west', 'center', 'north', 'south']; var elemClasses = contentEl.attr('class'); - var origLayoutClasses = ''; + var origLayoutClasses = []; if(elemClasses) { origLayoutClasses = $.grep( elemClasses.split(' '), @@ -557,4 +557,4 @@ var statusMessage = function(text, type) { var errorMessage = function(text) { jQuery.noticeAdd({text: text, type: 'error'}); -}; \ No newline at end of file +}; From db657046392b6840fc7d357fb0d2978649f4379a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 31 Mar 2012 20:08:25 +1300 Subject: [PATCH 25/44] MINOR Removed PHP 5.2 check in DateTest --- tests/model/DateTest.php | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/model/DateTest.php b/tests/model/DateTest.php index 00a344a1f..93aa4616f 100644 --- a/tests/model/DateTest.php +++ b/tests/model/DateTest.php @@ -6,31 +6,19 @@ class DateTest extends SapphireTest { protected $originalTZ; - + function setUp() { // Set timezone to support timestamp->date conversion. - // We can't use date_default_timezone_set() as its not supported prior to PHP 5.2 - - if (version_compare(PHP_VERSION, '5.2.0', '<')) { - $this->originalTZ = ini_get('date.timezone'); - ini_set('date.timezone', 'Pacific/Auckland'); - } else { - $this->originalTZ = date_default_timezone_get(); - date_default_timezone_set('Pacific/Auckland'); - } + $this->originalTZ = date_default_timezone_get(); + date_default_timezone_set('Pacific/Auckland'); parent::setUp(); } - + function tearDown() { - if(version_compare(PHP_VERSION, '5.2.0', '<') ){ - ini_set('date.timezone',$this->originalTZ); - } else { - date_default_timezone_set($this->originalTZ); - } - + date_default_timezone_set($this->originalTZ); parent::tearDown(); } - + function testNiceDate() { $this->assertEquals('31/03/2008', DBField::create('Date', 1206968400)->Nice(), "Date->Nice() works with timestamp integers" From 58e912d4d78eb512ca558197029bc7cc60533057 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 31 Mar 2012 20:08:54 +1300 Subject: [PATCH 26/44] MINOR Removed check for PHP versions less than 5.2 in Cookie --- control/Cookie.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/control/Cookie.php b/control/Cookie.php index 4f38a8957..0b4f8e1a7 100644 --- a/control/Cookie.php +++ b/control/Cookie.php @@ -21,22 +21,17 @@ class Cookie { * @param string $path See http://php.net/set_session * @param string $domain See http://php.net/set_session * @param boolean $secure See http://php.net/set_session - * @param boolean $httpOnly See http://php.net/set_session (PHP 5.2+ only) + * @param boolean $httpOnly See http://php.net/set_session */ static function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = false) { if(!headers_sent($file, $line)) { $expiry = $expiry > 0 ? time()+(86400*$expiry) : $expiry; $path = ($path) ? $path : Director::baseURL(); - - // Versions of PHP prior to 5.2 do not support the $httpOnly value - if(version_compare(phpversion(), 5.2, '<')) { - setcookie($name, $value, $expiry, $path, $domain, $secure); - } else { - setcookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); - } + setcookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); } else { - if(self::$report_errors) + if(self::$report_errors) { user_error("Cookie '$name' can't be set. The site started outputting was content at line $line in $file", E_USER_WARNING); + } } } From 68aaae8cc06e2c45c6b9af4a4bf0f646ffe292a7 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 31 Mar 2012 20:09:45 +1300 Subject: [PATCH 27/44] MINOR Update docs and version checking for PHP 5.3+ --- _config.php | 2 -- dev/install/install.php | 9 ++++----- dev/install/install.php5 | 2 +- dev/install/php5-required.html | 7 +++---- main.php | 7 +++++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/_config.php b/_config.php index fae926994..abecc340e 100644 --- a/_config.php +++ b/_config.php @@ -42,8 +42,6 @@ Director::addRules(20, array( Object::useCustomClass('SSDatetime', 'SS_Datetime', true); Object::useCustomClass('Datetime', 'SS_Datetime', true); - - /** * The root directory of TinyMCE */ diff --git a/dev/install/install.php b/dev/install/install.php index 65030accd..20c86a13d 100644 --- a/dev/install/install.php +++ b/dev/install/install.php @@ -1,26 +1,25 @@ @@ -20,10 +19,10 @@

PHP 5 required

-

To run SilverStripe, please install PHP 5.2 or greater.

+

To run SilverStripe, please install PHP 5.3 or greater.

We have detected that you are running PHP version $PHPVersion. In order to run SilverStripe, - you must have PHP version 5.2 or greater, and for best results we recommend PHP 5.3 or greater.

+ you must have PHP version 5.3 or greater.

If you are running on a shared host, you may need to ask your hosting provider how to do this.

@@ -37,4 +36,4 @@
- \ No newline at end of file + diff --git a/main.php b/main.php index 651e8416a..2e43b0a12 100644 --- a/main.php +++ b/main.php @@ -1,9 +1,10 @@ Date: Tue, 3 Apr 2012 10:28:07 +1200 Subject: [PATCH 28/44] MINOR Adjusted wording based on E_DEPRECATED and E_USER_DEPRECATED error levels. --- dev/DebugView.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/DebugView.php b/dev/DebugView.php index d29d9987a..7a094d6a9 100644 --- a/dev/DebugView.php +++ b/dev/DebugView.php @@ -31,11 +31,11 @@ class DebugView extends Object { 'class' => 'notice' ), E_DEPRECATED => array( - 'title' => 'Deprecation', + 'title' => 'Deprecated', 'class' => 'notice' ), E_USER_DEPRECATED => array( - 'title' => 'Deprecation', + 'title' => 'User Deprecated', 'class' => 'notice' ), E_CORE_ERROR => array( From f8a6db8d31c9d902b84bb07b4bd08c18d0215a43 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 3 Apr 2012 11:29:44 +1200 Subject: [PATCH 29/44] MINOR Temporarily reverted X-ControllerURL push state, as the header doesn't get set correctly in LeftAndMain::handleRequest() correctly. This fixes saving pages until X-ControllerURL has been corrected. --- admin/javascript/LeftAndMain.Content.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index d1cf3d221..fb6c8a391 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -164,13 +164,6 @@ self.submitForm_responseHandler(form, xmlhttp.responseText, status, xmlhttp, formData); } - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. - // Causes non-pushState browser to re-request the URL, so ignore for those. - if(window.History.enabled && !History.emulated.pushState) { - var url = xmlhttp.getResponseHeader('X-ControllerURL'); - if(url) window.History.replaceState({}, '', url); - } - // Re-init tabs (in case the form tag itself is a tabset) if(self.hasClass('ss-tabset')) self.removeClass('ss-tabset').addClass('ss-tabset'); @@ -317,4 +310,4 @@ } }); -})(jQuery); \ No newline at end of file +})(jQuery); From 68db977ef14583c4ac3cac747459fc5d0cde230c Mon Sep 17 00:00:00 2001 From: Simon Elvery Date: Tue, 3 Apr 2012 15:05:21 +1000 Subject: [PATCH 30/44] MINOR: Provide a setter for heading level on HeaderField object. --- forms/HeaderField.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/forms/HeaderField.php b/forms/HeaderField.php index 696df8cb1..669c197a8 100644 --- a/forms/HeaderField.php +++ b/forms/HeaderField.php @@ -35,6 +35,10 @@ class HeaderField extends DatalessField { public function getHeadingLevel() { return $this->headingLevel; } + + public function setHeadingLevel($level) { + $this->headingLevel = $level; + } function getAttributes() { return array_merge( From 59706d5bf5618a39e5274f5d819d6bf81ea1453d Mon Sep 17 00:00:00 2001 From: unclecheese Date: Tue, 3 Apr 2012 15:58:17 -0300 Subject: [PATCH 31/44] Using deprecated StringField::Upper() and StringField::Lower(). Calling $MyEnumField.Upper on the template returns an unresolvable deprecation error. --- model/fieldtypes/Enum.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/fieldtypes/Enum.php b/model/fieldtypes/Enum.php index da0865667..6c0eedc8b 100644 --- a/model/fieldtypes/Enum.php +++ b/model/fieldtypes/Enum.php @@ -87,10 +87,10 @@ class Enum extends DBField { } function Lower() { - return StringField::Lower(); + return StringField::LowerCase(); } function Upper() { - return StringField::Upper(); + return StringField::UpperCase(); } } From 076f1a83f4a012d218acb41d47be9a120aa1ae07 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 4 Apr 2012 16:00:56 +1200 Subject: [PATCH 32/44] BUGFIX Fixed GridField edit link appearing 9999px off screen, should not be visible as the icon replaces the link text. --- css/GridField.css | 2 +- scss/GridField.scss | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/css/GridField.css b/css/GridField.css index 8d8d74f78..9466eeaa2 100644 --- a/css/GridField.css +++ b/css/GridField.css @@ -17,7 +17,7 @@ .cms table.ss-gridfield-table tbody td button { border: none; background: none; margin: 0 0 0 2px; padding: 0; width: auto; text-shadow: none; } .cms table.ss-gridfield-table tbody td button.ui-state-hover { background: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } .cms table.ss-gridfield-table tbody td button.ui-state-active { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } -.cms table.ss-gridfield-table tbody td a.edit-link { display: inline-block; width: 16px; height: 20px; text-indent: 9999em; background: url(../images/icons/document--pencil.png) no-repeat 0 1px; } +.cms table.ss-gridfield-table tbody td a.edit-link { display: inline-block; width: 16px; height: 20px; text-indent: 9999em; overflow: hidden; vertical-align: middle; background: url(../images/icons/document--pencil.png) no-repeat 0 1px; } .cms table.ss-gridfield-table tfoot { color: #1d2224; } .cms table.ss-gridfield-table tfoot tr td { background: #95a5ab; padding: .7em; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .cms table.ss-gridfield-table tr.title { -moz-border-radius-topleft: 7px; -webkit-border-top-left-radius: 7px; -o-border-top-left-radius: 7px; -ms-border-top-left-radius: 7px; -khtml-border-top-left-radius: 7px; border-top-left-radius: 7px; -moz-border-radius-topright: 7px; -webkit-border-top-right-radius: 7px; -o-border-top-right-radius: 7px; -ms-border-top-right-radius: 7px; -khtml-border-top-right-radius: 7px; border-top-right-radius: 7px; } diff --git a/scss/GridField.scss b/scss/GridField.scss index c3cea9f9e..84a854587 100644 --- a/scss/GridField.scss +++ b/scss/GridField.scss @@ -129,9 +129,11 @@ $gf_grid_x: 16px; width:$gf_grid_x; height:20px; //min height to fit the edit icon text-indent:9999em; + overflow: hidden; + vertical-align: middle; background: url(../images/icons/document--pencil.png) no-repeat 0 1px; - } - } + } + } } tfoot { @@ -468,4 +470,4 @@ $gf_grid_x: 16px; border-left: 1px solid $gf_colour_border; } } -} \ No newline at end of file +} From 0414e42bbce51e721922f1dd7e658c7459f3bcda Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 18:09:03 +0200 Subject: [PATCH 33/44] MINOR Keep X-ControllerURL canonical by not re-constructing with question mark if there's no GET string (caused duplicate HTML5 pushState requests) --- admin/code/LeftAndMain.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index df88b41a3..851226124 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -332,7 +332,7 @@ class LeftAndMain extends Controller implements PermissionProvider { $url = $request->getURL(); if($getVars = $request->getVars()) { if(isset($getVars['url'])) unset($getVars['url']); - $url = Controller::join_links($url, '?' . http_build_query($getVars)); + $url = Controller::join_links($url, $getVars ? '?' . http_build_query($getVars) : ''); } $response->addHeader('X-ControllerURL', $url); } From c2b741642e832d68c082a2949d85a811234aebd0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 18:30:01 +0200 Subject: [PATCH 34/44] MINOR Moved X-ControllerURL handling into global ajax response handlers to avoid code duplication --- admin/javascript/LeftAndMain.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index ffdd578e6..6800ffaf7 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -32,8 +32,16 @@ jQuery.noConflict(); $(window).bind('resize', positionLoadingSpinner).trigger('resize'); - // global ajax error handlers + // global ajax handlers $.ajaxSetup({ + complete: function(xhr) { + // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. + // Causes non-pushState browser to re-request the URL, so ignore for those. + if(window.History.enabled && !History.emulated.pushState) { + var url = xmlhttp.getResponseHeader('X-ControllerURL'); + if(url) window.History.replaceState({}, '', url); + } + }, error: function(xmlhttp, status, error) { if(xmlhttp.status < 200 || xmlhttp.status > 399) { var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.statusText; @@ -87,13 +95,6 @@ jQuery.noConflict(); }); $('.cms-edit-form').live('reloadeditform', function(e, data) { - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it - // Causes non-pushState browser to re-request the URL, so ignore for those. - if(window.History.enabled && !History.emulated.pushState) { - var url = data.xmlhttp.getResponseHeader('X-ControllerURL'); - if(url) window.history.replaceState({}, '', url); - } - self.redraw(); }); @@ -251,13 +252,6 @@ jQuery.noConflict(); newContentEl.css('visibility', 'visible'); newContentEl.removeClass('loading'); - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. - // Causes non-pushState browser to re-request the URL, so ignore for those. - if(window.History.enabled && !History.emulated.pushState) { - var url = xhr.getResponseHeader('X-ControllerURL'); - if(url) window.History.replaceState({}, '', url); - } - self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: newContentEl}); }, error: function(xhr, status, e) { From ac6f9e998750a8e4bfaf74927390b97680a2756b Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 30 Mar 2012 18:30:54 +0200 Subject: [PATCH 35/44] MINOR Normalize trailing slashes in X-ControllerURL handling to avoid double requests caused by SS_HTTPRequest modifying the original URL (removing trailing slash etc) --- admin/javascript/LeftAndMain.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 6800ffaf7..5b393cd04 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -38,8 +38,12 @@ jQuery.noConflict(); // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. // Causes non-pushState browser to re-request the URL, so ignore for those. if(window.History.enabled && !History.emulated.pushState) { - var url = xmlhttp.getResponseHeader('X-ControllerURL'); - if(url) window.History.replaceState({}, '', url); + var url = xhr.getResponseHeader('X-ControllerURL'); + // Normalize trailing slashes in URL to work around routing weirdnesses in SS_HTTPRequest. + var isSame = (url && History.getPageUrl().replace(/\/+$/, '') == url.replace(/\/+$/, '')); + if(isSame) { + window.History.replaceState({}, '', url); + } } }, error: function(xmlhttp, status, error) { From 40d73127ae493e60b7c81991239805387aa00d2d Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 4 Apr 2012 16:59:30 +0200 Subject: [PATCH 36/44] MINOR Using late static binding instead of Object::create() calls --- admin/code/LeftAndMain.php | 4 ++-- admin/code/ModelAdmin.php | 6 ++--- admin/code/SecurityAdmin.php | 8 +++---- core/Convert.php | 2 +- core/Object.php | 6 ++--- dev/DevelopmentAdmin.php | 22 +++++++++---------- docs/en/topics/rich-text-editing.md | 2 +- docs/en/topics/security.md | 6 ++--- filesystem/File.php | 10 ++++----- filesystem/FileNameFilter.php | 4 ++-- filesystem/Folder.php | 2 +- filesystem/Upload.php | 2 +- forms/ComplexTableField.php | 2 +- forms/DateField.php | 4 ++-- forms/DatetimeField.php | 4 ++-- forms/HtmlEditorField.php | 2 +- forms/UploadField.php | 4 ++-- forms/gridfield/GridFieldDeleteAction.php | 4 ++-- forms/gridfield/GridFieldFilterHeader.php | 4 ++-- model/Image.php | 2 +- model/Transliterator.php | 8 ++----- model/URLSegmentFilter.php | 9 ++------ security/Group.php | 7 +++--- security/Member.php | 10 ++++----- security/Security.php | 3 +-- tests/core/ObjectTest.php | 14 ++++++------ .../gridfield/GridFieldDetailFormTest.php | 6 ++--- view/ViewableData.php | 2 +- 28 files changed, 73 insertions(+), 86 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 4a135574a..b93a468f0 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -969,7 +969,7 @@ class LeftAndMain extends Controller implements PermissionProvider { * Return the CMS's HTML-editor toolbar */ public function EditorToolbar() { - return Object::create('HtmlEditorField_Toolbar', $this, "EditorToolbar"); + return HtmlEditorField_Toolbar::create($this, "EditorToolbar"); } /** @@ -1032,7 +1032,7 @@ class LeftAndMain extends Controller implements PermissionProvider { 'BatchActionsForm', new FieldList( new HiddenField('csvIDs'), - Object::create('DropdownField', + DropdownField::create( 'Action', false, $actionsMap diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 131f4e192..202185caf 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -117,7 +117,7 @@ abstract class ModelAdmin extends LeftAndMain { $list = $this->getList(); $exportButton = new GridFieldExportButton(); $exportButton->setExportColumns($this->getExportFields()); - $listField = Object::create('GridField', + $listField = GridField::create( $this->modelClass, false, $list, @@ -180,9 +180,9 @@ abstract class ModelAdmin extends LeftAndMain { $form = new Form($this, "SearchForm", $context->getSearchFields(), new FieldList( - Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search')) + ResetFormAction::create('clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search')) ->setUseButtonTag(true)->addExtraClass('ss-ui-action-minor'), - Object::create('FormAction', 'search', _t('MemberTableField.SEARCH', 'Search')) + FormAction::create('search', _t('MemberTableField.SEARCH', 'Search')) ->setUseButtonTag(true) ), new RequiredFields() diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php index 556c761f1..97bc5dfcb 100755 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -43,7 +43,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { $record = $this->getRecord($id); if($record && !$record->canView()) return Security::permissionFailure($this); - $memberList = Object::create('GridField', + $memberList = GridField::create( 'Members', false, DataList::create('Member'), @@ -52,8 +52,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { )->addExtraClass("members_grid"); $memberListConfig->getComponentByType('GridFieldDetailForm')->setValidator(new Member_Validator()); - $groupList = Object::create('GridField', - 'Groups', + $groupList = GridField::create( 'Groups', false, DataList::create('Group'), GridFieldConfig_RecordEditor::create() @@ -104,8 +103,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { // Add roles editing interface if(Permission::check('APPLY_ROLES')) { - $rolesField = Object::create('GridField', - 'Roles', + $rolesField = GridField::create( 'Roles', false, DataList::create('PermissionRole'), GridFieldConfig_RecordEditor::create() diff --git a/core/Convert.php b/core/Convert.php index f4a8d9635..98abea47f 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -320,7 +320,7 @@ class Convert { * @return string */ public static function raw2url($title) { - $f = Object::create('URLSegmentFilter'); + $f = URLSegmentFilter::create(); return $f->filter($title); } } diff --git a/core/Object.php b/core/Object.php index 145e36222..4051da256 100755 --- a/core/Object.php +++ b/core/Object.php @@ -87,7 +87,7 @@ abstract class Object { * or calling on Object and passing the class name as the first parameter. The following * are equivalent: * $list = DataList::create('SiteTree'); - * $list = Object::create('DataList', 'SiteTree'); + * $list = DataList::create('SiteTree'); * * @param string $class the class name * @param mixed $arguments,... arguments to pass to the constructor @@ -121,7 +121,7 @@ abstract class Object { * are respected. * * `Object::create_from_string("Versioned('Stage','Live')")` will return the result of - * `Object::create('Versioned', 'Stage', 'Live);` + * `Versioned::create('Stage', 'Live);` * * It is designed for simple, clonable objects. The first time this method is called for a given * string it is cached, and clones of that object are returned. @@ -130,7 +130,7 @@ abstract class Object { * impossible to pass null as the firstArg argument. * * `Object::create_from_string("Varchar(50)", "MyField")` will return the result of - * `Object::create('Vachar', 'MyField', '50');` + * `Vachar::create('MyField', '50');` * * Arguments are always strings, although this is a quirk of the current implementation rather * than something that can be relied upon. diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index e3edecb76..f27ea647a 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -92,7 +92,7 @@ class DevelopmentAdmin extends Controller { // This action is sake-only right now. unset($actions["modules/add"]); - $renderer = Object::create('DebugView'); + $renderer = DebugView::create(); $renderer->writeHeader(); $renderer->writeInfo("Sapphire Development Tools", Director::absoluteBaseURL()); $base = Director::baseURL(); @@ -116,33 +116,33 @@ class DevelopmentAdmin extends Controller { } function tests($request) { - return Object::create('TestRunner'); + return TestRunner::create(); } function jstests($request) { - return Object::create('JSTestRunner'); + return JSTestRunner::create(); } function tasks() { - return Object::create('TaskRunner'); + return TaskRunner::create(); } function viewmodel() { - return Object::create('ModelViewer'); + return ModelViewer::create(); } function build($request) { if(Director::is_cli()) { - $da = Object::create('DatabaseAdmin'); + $da = DatabaseAdmin::create(); return $da->handleRequest($request, $this->model); } else { - $renderer = Object::create('DebugView'); + $renderer = DebugView::create(); $renderer->writeHeader(); $renderer->writeInfo("Environment Builder", Director::absoluteBaseURL()); echo "
"; echo "

Database is building.... Check below for any errors

Database has been built successfully

"; - $da = Object::create('DatabaseAdmin'); + $da = DatabaseAdmin::create(); return $da->handleRequest($request, $this->model); echo "
"; @@ -157,10 +157,10 @@ class DevelopmentAdmin extends Controller { * 'build/defaults' => 'buildDefaults', */ function buildDefaults() { - $da = Object::create('DatabaseAdmin'); + $da = DatabaseAdmin::create(); if (!Director::is_cli()) { - $renderer = Object::create('DebugView'); + $renderer = DebugView::create(); $renderer->writeHeader(); $renderer->writeInfo("Defaults Builder", Director::absoluteBaseURL()); echo "
"; @@ -189,6 +189,6 @@ class DevelopmentAdmin extends Controller { } function viewcode($request) { - return Object::create('CodeViewer'); + return CodeViewer::create(); } } diff --git a/docs/en/topics/rich-text-editing.md b/docs/en/topics/rich-text-editing.md index 731fd43f4..f9a900100 100644 --- a/docs/en/topics/rich-text-editing.md +++ b/docs/en/topics/rich-text-editing.md @@ -93,7 +93,7 @@ of the CMS you have to take care of instanciation yourself: // File: mysite/code/MyController.php class MyObjectController extends Controller { public function EditorToolbar() { - return Object::create('HtmlEditorField_Toolbar', $this, "EditorToolbar"); + return HtmlEditorField_Toolbar::create($this, "EditorToolbar"); } } diff --git a/docs/en/topics/security.md b/docs/en/topics/security.md index 16b0d5177..d1f37bcfb 100644 --- a/docs/en/topics/security.md +++ b/docs/en/topics/security.md @@ -213,8 +213,8 @@ PHP: public function search($request) { $htmlTitle = '

Your results for:' . Convert::raw2xml($request->getVar('Query')) . '

'; return $this->customise(array( - 'Query' => DBField::create('Text', $request->getVar('Query')), - 'HTMLTitle' => DBField::create('HTMLText', $htmlTitle) + 'Query' => Text::create($request->getVar('Query')), + 'HTMLTitle' => HTMLText::create($htmlTitle) )); } } @@ -243,7 +243,7 @@ PHP: $rssRelativeLink = "/rss?Query=" . urlencode($_REQUEST['query']) . "&sortOrder=asc"; $rssLink = Controller::join_links($this->Link(), $rssRelativeLink); return $this->customise(array( - "RSSLink" => DBField::create("Text", $rssLink), + "RSSLink" => Text::create($rssLink), )); } } diff --git a/filesystem/File.php b/filesystem/File.php index 8d93a0a7b..1e306a828 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -349,12 +349,12 @@ class File extends DataObject { } //create the file attributes in a FieldGroup - $filePreview = FormField::create('CompositeField', - FormField::create('CompositeField', + $filePreview = CompositeField::create( + CompositeField::create( $previewField )->setName("FilePreviewImage")->addExtraClass('cms-file-info-preview'), - FormField::create('CompositeField', - FormField::create('CompositeField', + CompositeField::create( + CompositeField::create( new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'), new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()), $urlField = new ReadonlyField('ClickableURL', _t('AssetTableField.URL','URL'), @@ -566,7 +566,7 @@ class File extends DataObject { if(!$name) $name = $this->Title; // Fix illegal characters - $filter = Object::create('FileNameFilter'); + $filter = FileNameFilter::create(); $name = $filter->filter($name); // We might have just turned it blank, so check again. diff --git a/filesystem/FileNameFilter.php b/filesystem/FileNameFilter.php index 1669ff371..ddfb3a1d2 100644 --- a/filesystem/FileNameFilter.php +++ b/filesystem/FileNameFilter.php @@ -26,7 +26,7 @@ * * See {@link URLSegmentFilter} for a more generic implementation. */ -class FileNameFilter { +class FileNameFilter extends Object { /** * @var Boolean @@ -100,7 +100,7 @@ class FileNameFilter { */ function getTransliterator() { if($this->transliterator === null && self::$default_use_transliterator) { - $this->transliterator = Object::create('Transliterator'); + $this->transliterator = Transliterator::create(); } return $this->transliterator; } diff --git a/filesystem/Folder.php b/filesystem/Folder.php index 3d540e1b3..26a65efb8 100644 --- a/filesystem/Folder.php +++ b/filesystem/Folder.php @@ -232,7 +232,7 @@ class Folder extends File { // $parentFolder = Folder::findOrMake("Uploads"); // Generate default filename - $nameFilter = Object::create('FileNameFilter'); + $nameFilter = FileNameFilter::create(); $file = $nameFilter->filter($tmpFile['name']); while($file[0] == '_' || $file[0] == '.') { $file = substr($file, 1); diff --git a/filesystem/Upload.php b/filesystem/Upload.php index 97a214933..3731c1563 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -131,7 +131,7 @@ class Upload extends Controller { } // Generate default filename - $nameFilter = Object::create('FileNameFilter'); + $nameFilter = FileNameFilter::create(); $file = $nameFilter->filter($tmpFile['name']); $fileName = basename($file); diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 1b75a56f2..9374ab003 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -816,7 +816,7 @@ class ComplexTableField_Popup extends Form { $actions = new FieldList(); if(!$readonly) { $actions->push( - Object::create('FormAction', + FormAction::create( "saveComplexTableField", _t('CMSMain.SAVE', 'Save') ) diff --git a/forms/DateField.php b/forms/DateField.php index a1f2c6b66..7345e6d39 100644 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -112,7 +112,7 @@ class DateField extends TextField { function FieldHolder() { // TODO Replace with properly extensible view helper system - $d = Object::create('DateField_View_JQuery', $this); + $d = DateField_View_JQuery::create($this); $d->onBeforeRender(); $html = parent::FieldHolder(); $html = $d->onAfterRender($html); @@ -459,7 +459,7 @@ class DateField_Disabled extends DateField { * @package sapphire * @subpackage forms */ -class DateField_View_JQuery { +class DateField_View_JQuery extends Object { protected $field; diff --git a/forms/DatetimeField.php b/forms/DatetimeField.php index 6fd774ba4..ee5487dcb 100644 --- a/forms/DatetimeField.php +++ b/forms/DatetimeField.php @@ -57,8 +57,8 @@ class DatetimeField extends FormField { function __construct($name, $title = null, $value = ""){ $this->config = self::$default_config; - $this->dateField = Object::create('DateField', $name . '[date]', false); - $this->timeField = Object::create('TimeField', $name . '[time]', false); + $this->dateField = DateField::create($name . '[date]', false); + $this->timeField = TimeField::create($name . '[time]', false); $this->timezoneField = new HiddenField($this->getName() . '[timezone]'); parent::__construct($name, $title, $value); diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index 3e688306c..3ca21adc7 100644 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -322,7 +322,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { ) ), new FieldList( - Object::create('ResetFormAction', 'remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link')) + ResetFormAction::create('remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link')) ->addExtraClass('ss-ui-action-destructive') ->setUseButtonTag(true) , diff --git a/forms/UploadField.php b/forms/UploadField.php index 85f643462..a4a3c7740 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -407,7 +407,7 @@ class UploadField extends FileField { * @return UploadField_ItemHandler */ public function getItemHandler($itemID) { - return Object::create('UploadField_ItemHandler', $this, $itemID); + return UploadField_ItemHandler::create($this, $itemID); } /** @@ -415,7 +415,7 @@ class UploadField extends FileField { * @return UploadField_ItemHandler */ public function handleSelect(SS_HTTPRequest $request) { - return Object::create('UploadField_SelectHandler', $this, $this->folderName); + return UploadField_SelectHandler::create($this, $this->folderName); } /** diff --git a/forms/gridfield/GridFieldDeleteAction.php b/forms/gridfield/GridFieldDeleteAction.php index dbee43372..3adc01338 100644 --- a/forms/gridfield/GridFieldDeleteAction.php +++ b/forms/gridfield/GridFieldDeleteAction.php @@ -98,7 +98,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio */ public function getColumnContent($gridField, $record, $columnName) { if($this->removeRelation) { - $field = Object::create('GridField_FormAction', $gridField, 'UnlinkRelation'.$record->ID, false, "unlinkrelation", array('RecordID' => $record->ID)) + $field = GridField_FormAction::create($gridField, 'UnlinkRelation'.$record->ID, false, "unlinkrelation", array('RecordID' => $record->ID)) ->addExtraClass('gridfield-button-unlink') ->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink")) ->setAttribute('data-icon', 'chain--minus'); @@ -106,7 +106,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio if(!$record->canDelete()) { return; } - $field = Object::create('GridField_FormAction', $gridField, 'DeleteRecord'.$record->ID, false, "deleterecord", array('RecordID' => $record->ID)) + $field = GridField_FormAction::create($gridField, 'DeleteRecord'.$record->ID, false, "deleterecord", array('RecordID' => $record->ID)) ->addExtraClass('gridfield-button-delete') ->setAttribute('title', _t('GridAction.Delete', "Delete")) ->setAttribute('data-icon', 'decline'); diff --git a/forms/gridfield/GridFieldFilterHeader.php b/forms/gridfield/GridFieldFilterHeader.php index 3f409a356..bb7afd441 100644 --- a/forms/gridfield/GridFieldFilterHeader.php +++ b/forms/gridfield/GridFieldFilterHeader.php @@ -123,11 +123,11 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan $field = new FieldGroup( $field, - Object::create('GridField_FormAction', $gridField, 'filter', false, 'filter', null) + GridField_FormAction::create($gridField, 'filter', false, 'filter', null) ->addExtraClass('ss-gridfield-button-filter') ->setAttribute('title', _t('GridField.Filter', "Filter")) , - Object::create('GridField_FormAction', $gridField, 'reset', false, 'reset', null) + GridField_FormAction::create($gridField, 'reset', false, 'reset', null) ->addExtraClass('ss-gridfield-button-reset') ->setAttribute('title', _t('GridField.ResetFilter', "Reset")) ); diff --git a/model/Image.php b/model/Image.php index 658bd5898..74fecc609 100644 --- a/model/Image.php +++ b/model/Image.php @@ -153,7 +153,7 @@ class Image extends File { } // Generate default filename - $nameFilter = Object::create('FileNameFilter'); + $nameFilter = FileNameFilter::create(); $file = $nameFilter->filter($tmpFile['name']); if(!$file) $file = "file.jpg"; diff --git a/model/Transliterator.php b/model/Transliterator.php index 09f3e6098..943171673 100644 --- a/model/Transliterator.php +++ b/model/Transliterator.php @@ -13,17 +13,13 @@ * @package sapphire * @subpackage model */ -class Transliterator { +class Transliterator extends Object { /** * Allow the use of iconv() to perform transliteration. Set to false to disable. * Even if this variable is true, iconv() won't be used if it's not installed. */ static $use_iconv = false; - - function __construct() { - // A constructor is necessary for Object::create() to work - } - + /** * Convert the given utf8 string to a safe ASCII source */ diff --git a/model/URLSegmentFilter.php b/model/URLSegmentFilter.php index b3cd7f428..2d4edce63 100644 --- a/model/URLSegmentFilter.php +++ b/model/URLSegmentFilter.php @@ -14,12 +14,7 @@ * * See {@link FileNameFilter} for similar implementation for filesystem-based URLs. */ -class URLSegmentFilter { - - /** - * Necessary to support {@link Object::create()} - */ - function __construct() {} +class URLSegmentFilter extends Object { /** * @var Boolean @@ -104,7 +99,7 @@ class URLSegmentFilter { */ function getTransliterator() { if($this->transliterator === null && self::$default_use_transliterator) { - $this->transliterator = Object::create('Transliterator'); + $this->transliterator = Transliterator::create(); } return $this->transliterator; } diff --git a/security/Group.php b/security/Group.php index c4127880e..7c7a45bfd 100755 --- a/security/Group.php +++ b/security/Group.php @@ -66,8 +66,7 @@ class Group extends DataObject { new TabSet("Root", new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'), new TextField("Title", $this->fieldLabel('Title')), - $parentidfield = Object::create('DropdownField', - 'ParentID', + $parentidfield = DropdownField::create( 'ParentID', $this->fieldLabel('Parent'), DataList::create('Group')->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs') )->setEmptyString(' ') @@ -100,7 +99,7 @@ class Group extends DataObject { $config->getComponentByType('GridFieldAddExistingAutocompleter') ->setResultsFormat('$Title ($Email)')->setSearchFields(array('FirstName', 'Surname', 'Email')); $config->getComponentByType('GridFieldDetailForm')->setValidator(new Member_Validator()); - $memberList = Object::create('GridField', 'Members',false, $this->Members(), $config)->addExtraClass('members_grid'); + $memberList = GridField::create('Members',false, $this->Members(), $config)->addExtraClass('members_grid'); // @todo Implement permission checking on GridField //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); $fields->addFieldToTab('Root.Members', $memberList); @@ -164,7 +163,7 @@ class Group extends DataObject { $inheritedRoleIDs = array(); } - $rolesField = Object::create('ListboxField', 'Roles', false, $allRoles->map()->toArray()) + $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray()) ->setMultiple(true) ->setDefaultItems($groupRoleIDs) ->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group')) diff --git a/security/Member.php b/security/Member.php index e118ae013..6a2d6861c 100644 --- a/security/Member.php +++ b/security/Member.php @@ -151,7 +151,7 @@ class Member extends DataObject implements TemplateGlobalProvider { if(!$admins) { // Leave 'Email' and 'Password' are not set to avoid creating // persistent logins in the database. See Security::setDefaultAdmin(). - $admin = Object::create('Member'); + $admin = Member::create(); $admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin'); $admin->write(); $admin->Groups()->add($adminGroup); @@ -480,13 +480,13 @@ class Member extends DataObject implements TemplateGlobalProvider { function sendInfo($type = 'signup', $data = null) { switch($type) { case "signup": - $e = Object::create('Member_SignupEmail'); + $e = Member_SignupEmail::create(); break; case "changePassword": - $e = Object::create('Member_ChangePasswordEmail'); + $e = Member_ChangePasswordEmail::create(); break; case "forgotPassword": - $e = Object::create('Member_ForgotPasswordEmail'); + $e = Member_ForgotPasswordEmail::create(); break; } @@ -1134,7 +1134,7 @@ class Member extends DataObject implements TemplateGlobalProvider { $groupsMap = DataList::create('Group')->map('ID', 'Breadcrumbs')->toArray(); asort($groupsMap); $fields->addFieldToTab('Root.Main', - Object::create('ListboxField', 'DirectGroups', singleton('Group')->i18n_plural_name()) + ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name()) ->setMultiple(true)->setSource($groupsMap) ); diff --git a/security/Security.php b/security/Security.php index 671f9f62a..a6540eec7 100644 --- a/security/Security.php +++ b/security/Security.php @@ -448,8 +448,7 @@ class Security extends Controller { * @return Form Returns the lost password form */ public function LostPasswordForm() { - return Object::create('MemberLoginForm', - $this, + return MemberLoginForm::create( $this, 'LostPasswordForm', new FieldList( new EmailField('Email', _t('Member.EMAIL', 'Email')) diff --git a/tests/core/ObjectTest.php b/tests/core/ObjectTest.php index 2aaa4fd0e..1eed68851 100644 --- a/tests/core/ObjectTest.php +++ b/tests/core/ObjectTest.php @@ -113,7 +113,7 @@ class ObjectTest extends SapphireTest { * Tests that {@link Object::create()} correctly passes all arguments to the new object */ public function testCreateWithArgs() { - $createdObj = Object::create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5'); + $createdObj = ObjectTest_CreateTest::create('arg1', 'arg2', array(), null, 'arg5'); $this->assertEquals($createdObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5')); $strongObj = Object::strong_create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5'); @@ -129,18 +129,18 @@ class ObjectTest extends SapphireTest { * Tests that {@link Object::useCustomClass()} correnctly replaces normal and strong objects */ public function testUseCustomClass() { - $obj1 = Object::create('ObjectTest_CreateTest'); + $obj1 = ObjectTest_CreateTest::create(); $this->assertTrue($obj1 instanceof ObjectTest_CreateTest); Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest2'); - $obj2 = Object::create('ObjectTest_CreateTest'); + $obj2 = ObjectTest_CreateTest::create(); $this->assertTrue($obj2 instanceof ObjectTest_CreateTest2); $obj2_2 = Object::strong_create('ObjectTest_CreateTest'); $this->assertTrue($obj2_2 instanceof ObjectTest_CreateTest); Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest3', true); - $obj3 = Object::create('ObjectTest_CreateTest'); + $obj3 = ObjectTest_CreateTest::create(); $this->assertTrue($obj3 instanceof ObjectTest_CreateTest3); $obj3_2 = Object::strong_create('ObjectTest_CreateTest'); @@ -269,12 +269,12 @@ class ObjectTest extends SapphireTest { } public function testParentClass() { - $this->assertEquals(Object::create('ObjectTest_MyObject')->parentClass(), 'Object'); + $this->assertEquals(ObjectTest_MyObject::create()->parentClass(), 'Object'); } public function testIsA() { - $this->assertTrue(Object::create('ObjectTest_MyObject') instanceof Object); - $this->assertTrue(Object::create('ObjectTest_MyObject') instanceof ObjectTest_MyObject); + $this->assertTrue(ObjectTest_MyObject::create() instanceof Object); + $this->assertTrue(ObjectTest_MyObject::create() instanceof ObjectTest_MyObject); } /** diff --git a/tests/forms/gridfield/GridFieldDetailFormTest.php b/tests/forms/gridfield/GridFieldDetailFormTest.php index d71655811..fca4688f6 100644 --- a/tests/forms/gridfield/GridFieldDetailFormTest.php +++ b/tests/forms/gridfield/GridFieldDetailFormTest.php @@ -144,7 +144,7 @@ class GridFieldDetailFormTest_Person extends DataObject implements TestOnly { $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('Categories', - Object::create('GridField', 'Categories', 'Categories', + GridField::create('Categories', 'Categories', $this->Categories(), GridFieldConfig_RelationEditor::create() ) @@ -166,7 +166,7 @@ class GridFieldDetailFormTest_PeopleGroup extends DataObject implements TestOnly $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('People', - Object::create('GridField', 'People', 'People', + GridField::create('People', 'People', $this->People(), GridFieldConfig_RelationEditor::create() ) @@ -188,7 +188,7 @@ class GridFieldDetailFormTest_Category extends DataObject implements TestOnly { $fields = parent::getCMSFields($params); // TODO No longer necessary once FormScaffolder uses GridField $fields->replaceField('People', - Object::create('GridField', 'People', 'People', + GridField::create('People', 'People', $this->People(), GridFieldConfig_RelationEditor::create() ) diff --git a/view/ViewableData.php b/view/ViewableData.php index ed41e2ad8..d626d5d68 100644 --- a/view/ViewableData.php +++ b/view/ViewableData.php @@ -717,7 +717,7 @@ class ViewableData_Debugger extends ViewableData { // check for an extra attached data if($this->object->hasMethod('data') && $this->object->data() != $this->object) { - $debug .= Object::create('ViewableData_Debugger', $this->object->data())->forTemplate(); + $debug .= ViewableData_Debugger::create($this->object->data())->forTemplate(); } return $debug; From 63ff91e41b6af077823794119fc8d62b37e64fea Mon Sep 17 00:00:00 2001 From: Normann Lou Date: Fri, 30 Mar 2012 10:10:59 +1300 Subject: [PATCH 37/44] MINOR Documentation for CMS tree and new SiteTree->getStatusFlags() --- .../_images/page_node_deleted_as_normal.png | Bin 0 -> 7145 bytes docs/en/_images/page_node_normal.png | Bin 0 -> 7145 bytes docs/en/_images/page_node_removed.png | Bin 0 -> 9418 bytes docs/en/_images/page_node_scheduled.png | Bin 0 -> 9409 bytes docs/en/_images/sss.png | Bin 0 -> 7400 bytes docs/en/_images/tree_node.png | Bin 0 -> 11360 bytes docs/en/howto/customize-cms-tree.md | 76 ++++++++++++++++++ docs/en/howto/index.md | 1 + docs/en/reference/cms-architecture.md | 1 + 9 files changed, 78 insertions(+) create mode 100644 docs/en/_images/page_node_deleted_as_normal.png create mode 100644 docs/en/_images/page_node_normal.png create mode 100644 docs/en/_images/page_node_removed.png create mode 100644 docs/en/_images/page_node_scheduled.png create mode 100644 docs/en/_images/sss.png create mode 100644 docs/en/_images/tree_node.png create mode 100644 docs/en/howto/customize-cms-tree.md diff --git a/docs/en/_images/page_node_deleted_as_normal.png b/docs/en/_images/page_node_deleted_as_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9424d6fc968e9728d1ad40b80163ffa876ce23 GIT binary patch literal 7145 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 literal 0 HcmV?d00001 diff --git a/docs/en/_images/page_node_normal.png b/docs/en/_images/page_node_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..0b9424d6fc968e9728d1ad40b80163ffa876ce23 GIT binary patch literal 7145 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 literal 0 HcmV?d00001 diff --git a/docs/en/_images/page_node_removed.png b/docs/en/_images/page_node_removed.png new file mode 100644 index 0000000000000000000000000000000000000000..9012670081c9976be3fd75bdb5a81f44e0f85d84 GIT binary patch literal 9418 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 literal 0 HcmV?d00001 diff --git a/docs/en/_images/tree_node.png b/docs/en/_images/tree_node.png new file mode 100644 index 0000000000000000000000000000000000000000..f9991155e93e2d5263f57ee52b5e00f8f555a78f GIT binary patch literal 11360 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]`, +which recursively collects all nodes based on various filtering criteria. +The node strictly just has to implement the `[api:Hierarchy]` extension, +but in the CMS usually is a `[api:SiteTree]` object. + +## Add status lozenges to tree nodes ## + +A tree node in CMS could be rendered with lot of extra information but a node title, such as a +link that wraps around the node title, a node's id which is given as id attribute of the node +<li> tag, a extra checkbox beside the tree title, tree icon class or extra <span> +tags showing the node status, etc. SilverStripe tree node will be typically rendered into html +code like this: + + :::ss + ... + + ... + +By applying the proper style sheet, the snippet html above could produce the look of: +![Page Node Screenshot](../_images/tree_node.png "Page Node") + +SiteTree is a `[api:DataObject]` which is versioned by `[api:Versioned]` extension. +Each node can optionally have publication status flags, e.g. "Removed from draft". +Each flag has a unique identifier, which is also used as a CSS class for easier styling. + +Developers can easily add a new flag, delete or alter an existing flag on how it is looked +or changing the flag label. The customization of these lozenges could be done either through +inherited subclass or `[api:DataExtension]`. It is just really about how we change the return +value of function `SiteTree->getTreeTitle()` by two easily extendable methods +`SiteTree->getStatusClass()` and `SiteTree->getStatusFlags()`. + +Note: Though the flag is not necessarily tie to its status of __publication__ and it could +be used for flagging anything you like, we should keep this lozenge to show version-related +status, while let `SiteTree->CMSTreeClasses()` to deal with other customised classes, which +will be used for the class attribute of <li> tag of the tree node. + +### Add new flag ### +__Example: using a subclass__ + + :::php + class Page extends SiteTree { + function getScheduledToPublish(){ + // return either true or false + } + + function getStatusFlags(){ + $flags = parent::getStatusFlags(); + $flags['scheduledtopublish'] = "Scheduled To Publish"; + return $flags; + } + } + +The above subclass of `[api:SiteTree]` will add a new flag for indicating its +__'Scheduled To Publish'__ status. The look of the page node will be changed +from ![Normal Page Node](../_images/page_node_normal.png") to ![Scheduled Page Node](../_images/page_node_scheduled.png). The getStatusFlags has an `updateStatusFlags()` +extension point, so the flags can be modified through `DataExtension` rather than +inheritance as well. Deleting existing flags works by simply unsetting the array key. \ No newline at end of file diff --git a/docs/en/howto/index.md b/docs/en/howto/index.md index 5a3308986..6c5e979a1 100644 --- a/docs/en/howto/index.md +++ b/docs/en/howto/index.md @@ -11,6 +11,7 @@ the language and functions which are used in the guides. * [Grouping DataObjectSets](grouping-dataobjectsets). Group results in a [api:DataObjectSet] to create sub sections. * [PHPUnit Configuration](phpunit-configuration). How to setup your testing environment with PHPUnit * [Extend the CMS Interface](extend-cms-interface). +* [How to customize CMS Tree](customize-cms-tree). ## Feedback diff --git a/docs/en/reference/cms-architecture.md b/docs/en/reference/cms-architecture.md index 63f6117ff..076b5f8f3 100644 --- a/docs/en/reference/cms-architecture.md +++ b/docs/en/reference/cms-architecture.md @@ -259,5 +259,6 @@ Note: You can see any additional HTTP headers through the web developer tools in ## Related * [Howto: Extend the CMS Interface](../howto/extend-cms-interface) + * [Howto: Customize the CMS tree](../howto/customize-cms-tree) * [Reference: ModelAdmin](../reference/modeladmin) * [Topics: Rich Text Editing](../topics/rich-text-editing) \ No newline at end of file From 6f89fe0703904126953a7e31a8cbbb5d9f5a3779 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 5 Apr 2012 16:50:49 +1200 Subject: [PATCH 38/44] BUGFIX Show/hide correct fields when inserting a link in HtmlEditorField --- javascript/HtmlEditorField.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/javascript/HtmlEditorField.js b/javascript/HtmlEditorField.js index e6852b574..cee0c3559 100644 --- a/javascript/HtmlEditorField.js +++ b/javascript/HtmlEditorField.js @@ -340,7 +340,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; redraw: function(setDefaults) { this._super(); - var linkType = this.find(':input[name=LinkType]:checked').val(), list = ['internal', 'external', 'file', 'email'], i, item; + var linkType = this.find(':input[name=LinkType]:checked').val(), list = ['internal', 'external', 'file', 'email']; // If we haven't selected an existing link, then just make sure we default to "internal" for the link type. if(!linkType) { @@ -351,11 +351,17 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; this.addAnchorSelector(); // Toggle field visibility and state based on type selection - for(i=0;item==list[i];i++) jQuery(this.find('.field#' + item)).toggle(item == linkType); - jQuery(this.find('.field#Anchor')).toggle(linkType == 'internal' || linkType == 'anchor'); - jQuery(this.find('.field#AnchorSelector')).toggle(linkType=='anchor'); - jQuery(this.find('.field#AnchorRefresh')).toggle(linkType=='anchor'); + this.find('.field').hide(); + this.find('.field#LinkType').show(); + this.find('.field#' + linkType).show(); + if(linkType == 'internal' || linkType == 'anchor') this.find('.field#Anchor').show(); + if(linkType == 'anchor') { + this.find('.field#AnchorSelector').show(); + this.find('.field#AnchorRefresh').show(); + } + this.find(':input[name=TargetBlank]').attr('disabled', (linkType == 'email')); + if(typeof setDefaults == 'undefined' || setDefaults) { this.find(':input[name=TargetBlank]').attr('checked', (linkType == 'file')); } From a44b67bae2229acbcd374847615e1d1338c99f6c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 5 Apr 2012 14:44:42 +0200 Subject: [PATCH 39/44] API CHANGE Moved RequestHandler->isAjax() to SS_HTTPRequest->isAjax() --- admin/code/CMSBatchActionHandler.php | 2 +- admin/code/LeftAndMain.php | 4 +- control/Controller.php | 11 ----- control/Director.php | 2 +- control/HTTPRequest.php | 14 +++++++ control/RequestHandler.php | 53 +++++++++++++++++++++++++ forms/gridfield/GridFieldDetailForm.php | 2 +- tests/control/HTTPRequestTest.php | 12 ++++++ 8 files changed, 84 insertions(+), 16 deletions(-) diff --git a/admin/code/CMSBatchActionHandler.php b/admin/code/CMSBatchActionHandler.php index 4bc6c4f3b..23ee16129 100644 --- a/admin/code/CMSBatchActionHandler.php +++ b/admin/code/CMSBatchActionHandler.php @@ -68,7 +68,7 @@ class CMSBatchActionHandler extends RequestHandler { function handleAction($request) { // This method can't be called without ajax. - if(!$this->parentController->isAjax()) { + if(!$request->isAjax()) { $this->parentController->redirectBack(); return; } diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index b93a468f0..35ea95489 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -193,7 +193,7 @@ class LeftAndMain extends Controller implements PermissionProvider { if(Director::redirected_to()) return; // Audit logging hook - if(empty($_REQUEST['executeForm']) && !$this->isAjax()) $this->extend('accessedCMS'); + if(empty($_REQUEST['executeForm']) && !$this->request->isAjax()) $this->extend('accessedCMS'); // Set the members html editor config HtmlEditorConfig::set_active(Member::currentUser()->getHtmlEditorConfigForCMS()); @@ -341,7 +341,7 @@ class LeftAndMain extends Controller implements PermissionProvider { } function index($request) { - return ($this->isAjax()) ? $this->show($request) : $this->getViewer('index')->process($this); + return ($request->isAjax()) ? $this->show($request) : $this->getViewer('index')->process($this); } diff --git a/control/Controller.php b/control/Controller.php index 0f7b124e3..8071387dd 100644 --- a/control/Controller.php +++ b/control/Controller.php @@ -535,17 +535,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider { $this->session = $session; } - /** - * Returns true if this controller is processing an ajax request - * @return boolean True if this controller is processing an ajax request - */ - function isAjax() { - return ( - isset($this->requestParams['ajax']) || isset($_REQUEST['ajax']) || - (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest") - ); - } - /** * Joins two or more link segments together, putting a slash between them if necessary. * Use this for building the results of {@link Link()} methods. diff --git a/control/Director.php b/control/Director.php index 9c2b33745..980fa3588 100644 --- a/control/Director.php +++ b/control/Director.php @@ -682,7 +682,7 @@ class Director implements TemplateGlobalProvider { */ static function is_ajax() { if(Controller::has_curr()) { - return Controller::curr()->isAjax(); + return Controller::curr()->getRequest()->isAjax(); } else { return ( isset($_REQUEST['ajax']) || diff --git a/control/HTTPRequest.php b/control/HTTPRequest.php index 06bc90063..8a34ba3ad 100644 --- a/control/HTTPRequest.php +++ b/control/HTTPRequest.php @@ -232,6 +232,20 @@ class SS_HTTPRequest implements ArrayAccess { function getURL() { return ($this->getExtension()) ? $this->url . '.' . $this->getExtension() : $this->url; } + + /** + * Returns true if this request an ajax request, + * based on custom HTTP ajax added by common JavaScript libraries, + * or based on an explicit "ajax" request parameter. + * + * @return boolean + */ + function isAjax() { + return ( + $this->requestVar('ajax') || + $this->getHeader('X-Requested-With') && $this->getHeader('X-Requested-With') == "XMLHttpRequest" + ); + } /** * Enables the existence of a key-value pair in the request to be checked using diff --git a/control/RequestHandler.php b/control/RequestHandler.php index 73a5bc1dc..6167e19bb 100644 --- a/control/RequestHandler.php +++ b/control/RequestHandler.php @@ -338,6 +338,59 @@ class RequestHandler extends ViewableData { throw $e; } + + /** + * @deprecated 3.0 Use SS_HTTPRequest->isAjax() instead (through Controller->getRequest()) + */ + function isAjax() { + Deprecation::notice('3.0', 'Use SS_HTTPRequest->isAjax() instead (through Controller->getRequest())'); + return $this->request->isAjax(); + } + + /** + * Handle the X-Get-Fragment header that AJAX responses may provide, returning the + * fragment, or, in the case of non-AJAX form submissions, redirecting back to the submitter. + * + * X-Get-Fragment ensures that users won't end up seeing the unstyled form HTML in their browser + * If a JS error prevents the Ajax overriding of form submissions from happening. It also provides + * better non-JS operation. + * + * Out of the box, the handler "CurrentForm" value, which will return the rendered form. Non-Ajax + * calls will redirect back. + * + * To extend its responses, pass a map to the $options argument. Each key is the value of X-Get-Fragment + * that will work, and the value is a PHP 'callable' value that will return the response for that + * value. + * + * If you specify $options['default'], this will be used as the non-ajax response. + * + * Note that if you use handleFragmentResponse, then any Ajax requests will have to include X-Get-Fragment + * or an error will be thrown. + */ + function handleFragmentResponse($form, $options = array()) { + // Prepare the default options and combine with the others + $lOptions = array( + 'currentform' => array($form, 'forTemplate'), + 'default' => array('Director', 'redirectBack'), + ); + if($options) foreach($options as $k => $v) { + $lOptions[strtolower($k)] = $v; + } + + if($fragment = $this->request->getHeader('X-Get-Fragment')) { + $fragment = strtolower($fragment); + if(isset($lOptions[$fragment])) { + return call_user_func($lOptions[$fragment]); + } else { + throw new SS_HTTPResponse_Exception("X-Get-Fragment = '$fragment' not supported for this URL.", 400); + } + + } else { + if($this->isAjax()) throw new SS_HTTPResponse_Exception("Ajax requests to this URL require an X-Get-Fragment header.", 400); + return call_user_func($lOptions['default']); + } + + } /** * Returns the SS_HTTPRequest object that this controller is using. diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index a3585a2e3..58bd39d5c 100755 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -196,7 +196,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { 'ItemEditForm' => $form, ))->renderWith($this->template); - if($controller->isAjax()) { + if($request->isAjax()) { return $return; } else { // If not requested by ajax, we need to render it within the controller context+template diff --git a/tests/control/HTTPRequestTest.php b/tests/control/HTTPRequestTest.php index 4ae6b1b0a..fa92643f1 100644 --- a/tests/control/HTTPRequestTest.php +++ b/tests/control/HTTPRequestTest.php @@ -230,4 +230,16 @@ class HTTPRequestTest extends SapphireTest { 'Nested GET parameters should supplement POST parameters' ); } + + function testIsAjax() { + $req = new SS_HTTPRequest('GET', '/', array('ajax' => 0)); + $this->assertFalse($req->isAjax()); + + $req = new SS_HTTPRequest('GET', '/', array('ajax' => 1)); + $this->assertTrue($req->isAjax()); + + $req = new SS_HTTPRequest('GET', '/'); + $req->addHeader('X-Requested-With', 'XMLHttpRequest'); + $this->assertTrue($req->isAjax()); + } } From f97804bbe2a95b232de0417ce4cc6d95c45fc23f Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 5 Apr 2012 22:10:25 +0200 Subject: [PATCH 40/44] MINOR Fixed specificity of .add-form behaviour --- admin/javascript/LeftAndMain.AddForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/javascript/LeftAndMain.AddForm.js b/admin/javascript/LeftAndMain.AddForm.js index 9222404c0..5c781d6a5 100644 --- a/admin/javascript/LeftAndMain.AddForm.js +++ b/admin/javascript/LeftAndMain.AddForm.js @@ -13,7 +13,7 @@ * ss.i18n * .cms-edit-form */ - $('.add-form').entwine({ + $('.cms-edit-form.cms-add-form').entwine({ /** * Variable: Tree * (DOMElement) From 72985b6f42d51844c0bed63a14bad00b18a6a76c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 5 Apr 2012 22:12:00 +0200 Subject: [PATCH 41/44] MINOR Artificially triggering onsubmit event on CMS form buttons rather than calling submitForm() method, in order to give forms like .cms-add-form the option to overload its behaviour --- admin/javascript/LeftAndMain.EditForm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/javascript/LeftAndMain.EditForm.js b/admin/javascript/LeftAndMain.EditForm.js index f2bf9f225..4083f87ca 100644 --- a/admin/javascript/LeftAndMain.EditForm.js +++ b/admin/javascript/LeftAndMain.EditForm.js @@ -141,8 +141,8 @@ * * Suppress submission unless it is handled through ajaxSubmit(). */ - onsubmit: function(e) { - this.parents('.cms-content').submitForm(this); + onsubmit: function(e, button) { + this.parents('.cms-content').submitForm(this, button); return false; }, @@ -179,7 +179,7 @@ * Function: onclick */ onclick: function(e) { - $('.cms-content').submitForm(this.parents('form'), this); + this.parents('form').trigger('submit', [this]); e.preventDefault(); return false; } From e01b0aa3d03de29510c6211981e30af333acafe9 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sat, 24 Mar 2012 15:19:02 +1300 Subject: [PATCH 42/44] ENHANCEMENT PjaxResponseNegotiator for more structured partial ajax refreshes, applied in CMS and GridField. Also fixes issues with history.pushState() and pseudo-redirects on form submissions (e.g. from page/add to page/edit/show/) --- admin/code/LeftAndMain.php | 89 ++++++++++++-------- admin/javascript/LeftAndMain.AddForm.js | 9 +- admin/javascript/LeftAndMain.Content.js | 12 +-- admin/javascript/LeftAndMain.js | 56 +++++++----- control/PjaxResponseNegotiator.php | 69 +++++++++++++++ control/RequestHandler.php | 45 ---------- javascript/GridField.js | 4 +- tests/control/PjaxResponseNegotiatorTest.php | 22 +++++ 8 files changed, 197 insertions(+), 109 deletions(-) create mode 100644 control/PjaxResponseNegotiator.php create mode 100644 tests/control/PjaxResponseNegotiatorTest.php diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 35ea95489..9d6af1e39 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -104,6 +104,11 @@ class LeftAndMain extends Controller implements PermissionProvider { 'css' => array(), 'themedcss' => array(), ); + + /** + * @var PJAXResponseNegotiator + */ + protected $responseNegotiator; /** * @param Member $member @@ -328,20 +333,29 @@ class LeftAndMain extends Controller implements PermissionProvider { $response = parent::handleRequest($request, $model); if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class); if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', $title); - if(!$response->getHeader('X-ControllerURL')) { - $url = $request->getURL(); - if($getVars = $request->getVars()) { - if(isset($getVars['url'])) unset($getVars['url']); - $url = Controller::join_links($url, $getVars ? '?' . http_build_query($getVars) : ''); - } - $response->addHeader('X-ControllerURL', $url); - } return $response; } + /** + * Overloaded redirection logic to trigger a fake redirect on ajax requests. + * While this violates HTTP principles, its the only way to work around the + * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible. + * In isolation, that's not a problem - but combined with history.pushState() + * it means we would request the same redirection URL twice if we want to update the URL as well. + * See LeftAndMain.js for the required jQuery ajaxComplete handlers. + */ + function redirect($url, $code=302) { + if($this->request->isAjax()) { + $this->response->addHeader('X-ControllerURL', $url); + return ''; // Actual response will be re-requested by client + } else { + parent::redirect($url, $code); + } + } + function index($request) { - return ($request->isAjax()) ? $this->show($request) : $this->getViewer('index')->process($this); + return $this->getResponseNegotiator()->respond($request); } @@ -391,20 +405,30 @@ class LeftAndMain extends Controller implements PermissionProvider { public function show($request) { // TODO Necessary for TableListField URLs to work properly if($request->param('ID')) $this->setCurrentPageID($request->param('ID')); - - if($this->isAjax()) { - if($request->getVar('cms-view-form')) { - $form = $this->getEditForm(); - $content = $form->forTemplate(); - } else { - // Rendering is handled by template, which will call EditForm() eventually - $content = $this->renderWith($this->getTemplatesWithSuffix('_Content')); - } - } else { - $content = $this->renderWith($this->getViewer('show')); + return $this->getResponseNegotiator()->respond($request); + } + + /** + * Caution: Volatile API. + * + * @return PJAXResponseNegotiator + */ + protected function getResponseNegotiator() { + if(!$this->responseNegotiator) { + $controller = $this; + $this->responseNegotiator = new PJAXResponseNegotiator(array( + 'CurrentForm' => function() use(&$controller) { + return $controller->getEditForm()->forTemplate(); + }, + 'Content' => function() use(&$controller) { + return $controller->renderWith($controller->getTemplatesWithSuffix('_Content')); + }, + 'default' => function() use(&$controller) { + return $controller->renderWith($controller->getViewer('show')); + } + )); } - - return $content; + return $this->responseNegotiator; } //------------------------------------------------------------------------------------------// @@ -680,13 +704,10 @@ class LeftAndMain extends Controller implements PermissionProvider { $form->saveInto($record, true); $record->write(); $this->extend('onAfterSave', $record); - + $this->setCurrentPageID($record->ID); + $this->response->addHeader('X-Status', _t('LeftAndMain.SAVEDUP')); - - // write process might've changed the record, so we reload before returning - $form = $this->getEditForm($record->ID); - - return $form->forTemplate(); + return $this->getResponseNegotiator()->respond($request); } public function delete($data, $form) { @@ -697,12 +718,12 @@ class LeftAndMain extends Controller implements PermissionProvider { if(!$record || !$record->ID) throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404); $record->delete(); - - if($this->isAjax()) { - return $this->EmptyForm()->forTemplate(); - } else { - $this->redirectBack(); - } + + $this->response->addHeader('X-Status', _t('LeftAndMain.SAVEDUP')); + return $this->getResponseNegotiator()->respond( + $request, + array('currentform' => array($this, 'EmptyForm')) + ); } /** diff --git a/admin/javascript/LeftAndMain.AddForm.js b/admin/javascript/LeftAndMain.AddForm.js index 5c781d6a5..148bf9966 100644 --- a/admin/javascript/LeftAndMain.AddForm.js +++ b/admin/javascript/LeftAndMain.AddForm.js @@ -87,7 +87,7 @@ var data = this.serializeArray(); data.push({name:'Suffix',value:newPages[parentID]++}); data.push({name:button.attr('name'),value:button.val()}); - + // TODO Should be set by hiddenfield already jQuery('.cms-content').entwine('ss').loadForm( this.attr('action'), @@ -96,7 +96,12 @@ // Tree updates are triggered by Form_EditForm load events button.removeClass('loading'); }, - {type: 'POST', data: data} + { + type: 'POST', + data: data, + // Refresh the whole area to avoid reloading just the form, without the tree around it + headers: {'X-Pjax': 'Content'} + } ); this.setNewPages(newPages); diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index fb6c8a391..16e80272d 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -55,16 +55,18 @@ this.trigger('loadform', {form: form, url: url}); - return jQuery.ajax(jQuery.extend({ - url: url, + var opts = jQuery.extend({}, { // Ensure that form view is loaded (rather than whole "Content" template) - data: {'cms-view-form': 1}, + headers: {"X-Pjax" : "CurrentForm"}, + url: url, complete: function(xmlhttp, status) { self.loadForm_responseHandler(form, xmlhttp.responseText, status, xmlhttp); if(callback) callback.apply(self, arguments); }, dataType: 'html' - }, ajaxOptions)); + }, ajaxOptions); + + return jQuery.ajax(opts); }, /** @@ -148,6 +150,7 @@ formData.push({name: 'BackURL', value:History.getPageUrl()}); jQuery.ajax(jQuery.extend({ + headers: {"X-Pjax" : "CurrentForm"}, url: form.attr('action'), data: formData, type: 'POST', @@ -289,7 +292,6 @@ if($.path.isExternal($(node).find('a:first'))) url = url = $.path.makeUrlAbsolute(url, $('base').attr('href')); // Reload only edit form if it exists (side-by-side view of tree and edit view), otherwise reload whole panel if(container.find('.cms-edit-form').length) { - url += '?cms-view-form=1'; container.entwine('ss').loadPanel(url, null, {selector: '.cms-edit-form'}); } else { container.entwine('ss').loadPanel(url); diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 5b393cd04..6c989144e 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -33,28 +33,29 @@ jQuery.noConflict(); $(window).bind('resize', positionLoadingSpinner).trigger('resize'); // global ajax handlers - $.ajaxSetup({ - complete: function(xhr) { - // Simulates a redirect on an ajax response - just exchange the URL without re-requesting it. - // Causes non-pushState browser to re-request the URL, so ignore for those. - if(window.History.enabled && !History.emulated.pushState) { - var url = xhr.getResponseHeader('X-ControllerURL'); - // Normalize trailing slashes in URL to work around routing weirdnesses in SS_HTTPRequest. - var isSame = (url && History.getPageUrl().replace(/\/+$/, '') == url.replace(/\/+$/, '')); - if(isSame) { - window.History.replaceState({}, '', url); - } + $(document).ajaxComplete(function(e, xhr, settings) { + // Simulates a redirect on an ajax response. + if(window.History.enabled) { + var url = xhr.getResponseHeader('X-ControllerURL'); + // Normalize trailing slashes in URL to work around routing weirdnesses in SS_HTTPRequest. + var isSame = (url && History.getPageUrl().replace(/\/+$/, '') == url.replace(/\/+$/, '')); + if(url && !isSame) { + var opts = { + pjax: settings.headers ? settings.headers['X-Pjax'] : null, + selector: settings.headers ? settings.headers['X-Pjax-Selector'] : null + }; + window.History.pushState(opts, '', url); } - }, - error: function(xmlhttp, status, error) { - if(xmlhttp.status < 200 || xmlhttp.status > 399) { - var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.statusText; - } else { - msg = error; - } - statusMessage(msg, 'bad'); } }); + $(document).ajaxError(function(e, xhr, settings, error) { + if(xhr.status < 200 || xhr.status > 399) { + var msg = (xhr.getResponseHeader('X-Status')) ? xhr.getResponseHeader('X-Status') : xhr.statusText; + } else { + msg = error; + } + statusMessage(msg, 'bad'); + }); /** * Main LeftAndMain interface with some control panel and an edit form. @@ -147,8 +148,8 @@ jQuery.noConflict(); loadPanel: function(url, title, data) { if(!data) data = {}; if(!title) title = ""; - - var selector = data.selector || '.cms-content', contentEl = $(selector); + if(!data.selector) data.selector = '.cms-content'; + var contentEl = $(data.selector); // Check change tracking (can't use events as we need a way to cancel the current state change) var trackedEls = contentEl.find(':data(changetracker)').add(contentEl.filter(':data(changetracker)')); @@ -209,8 +210,21 @@ jQuery.noConflict(); state: state, element: contentEl }); + var headers = {}; + if(state.data.pjax) { + headers['X-Pjax'] = state.data.pjax; + } else if(contentEl[0] != null && contentEl.is('form')) { + // Replace a form + headers["X-Pjax"] = 'CurrentForm'; + } else { + // Replace full RHS content area + headers["X-Pjax"] = 'Content'; + } + headers['X-Pjax-Selector'] = selector; + contentEl.addClass('loading'); var xhr = $.ajax({ + headers: headers, url: state.url, success: function(data, status, xhr) { // Update title diff --git a/control/PjaxResponseNegotiator.php b/control/PjaxResponseNegotiator.php new file mode 100644 index 000000000..c429bf6d0 --- /dev/null +++ b/control/PjaxResponseNegotiator.php @@ -0,0 +1,69 @@ +redirectBack() + 'default' => array('Director', 'redirectBack'), + ); + + /** + * @param RequestHandler $controller + * @param Array $callbacks + */ + function __construct($callbacks = array()) { + $this->callbacks = $callbacks; + } + + /** + * Out of the box, the handler "CurrentForm" value, which will return the rendered form. + * Non-Ajax calls will redirect back. + * + * @param SS_HTTPRequest $request + * @param array $extraCallbacks List of anonymous functions or callables returning either a string + * or SS_HTTPResponse, keyed by their fragment identifier. The 'default' key can + * be used as a fallback for non-ajax responses. + * @return SS_HTTPResponse + */ + public function respond(SS_HTTPRequest $request, $extraCallbacks = array()) { + // Prepare the default options and combine with the others + $callbacks = array_merge( + array_change_key_case($this->callbacks, CASE_LOWER), + array_change_key_case($extraCallbacks, CASE_LOWER) + ); + + if($fragment = $request->getHeader('X-Pjax')) { + $fragment = strtolower($fragment); + if(isset($callbacks[$fragment])) { + return call_user_func($callbacks[$fragment]); + } else { + throw new SS_HTTPResponse_Exception("X-Pjax = '$fragment' not supported for this URL.", 400); + } + } else { + if($request->isAjax()) throw new SS_HTTPResponse_Exception("Ajax requests to this URL require an X-Pjax header.", 400); + return call_user_func($callbacks['default']); + } + + } + + /** + * @param String $fragment + * @param Callable $callback + */ + public function setCallback($fragment, $callback) { + $this->callbacks[$fragment] = $callback; + } +} \ No newline at end of file diff --git a/control/RequestHandler.php b/control/RequestHandler.php index 6167e19bb..f73e53c85 100644 --- a/control/RequestHandler.php +++ b/control/RequestHandler.php @@ -347,51 +347,6 @@ class RequestHandler extends ViewableData { return $this->request->isAjax(); } - /** - * Handle the X-Get-Fragment header that AJAX responses may provide, returning the - * fragment, or, in the case of non-AJAX form submissions, redirecting back to the submitter. - * - * X-Get-Fragment ensures that users won't end up seeing the unstyled form HTML in their browser - * If a JS error prevents the Ajax overriding of form submissions from happening. It also provides - * better non-JS operation. - * - * Out of the box, the handler "CurrentForm" value, which will return the rendered form. Non-Ajax - * calls will redirect back. - * - * To extend its responses, pass a map to the $options argument. Each key is the value of X-Get-Fragment - * that will work, and the value is a PHP 'callable' value that will return the response for that - * value. - * - * If you specify $options['default'], this will be used as the non-ajax response. - * - * Note that if you use handleFragmentResponse, then any Ajax requests will have to include X-Get-Fragment - * or an error will be thrown. - */ - function handleFragmentResponse($form, $options = array()) { - // Prepare the default options and combine with the others - $lOptions = array( - 'currentform' => array($form, 'forTemplate'), - 'default' => array('Director', 'redirectBack'), - ); - if($options) foreach($options as $k => $v) { - $lOptions[strtolower($k)] = $v; - } - - if($fragment = $this->request->getHeader('X-Get-Fragment')) { - $fragment = strtolower($fragment); - if(isset($lOptions[$fragment])) { - return call_user_func($lOptions[$fragment]); - } else { - throw new SS_HTTPResponse_Exception("X-Get-Fragment = '$fragment' not supported for this URL.", 400); - } - - } else { - if($this->isAjax()) throw new SS_HTTPResponse_Exception("Ajax requests to this URL require an X-Get-Fragment header.", 400); - return call_user_func($lOptions['default']); - } - - } - /** * Returns the SS_HTTPRequest object that this controller is using. * Returns a placeholder {@link NullHTTPRequest} object unless diff --git a/javascript/GridField.js b/javascript/GridField.js index f5ce69eb0..b0a41dc5a 100644 --- a/javascript/GridField.js +++ b/javascript/GridField.js @@ -23,7 +23,7 @@ form.addClass('loading'); $.ajax($.extend({}, { - headers: {"X-Get-Fragment" : 'CurrentField'}, + headers: {"X-Pjax" : 'CurrentField'}, type: "POST", url: this.data('url'), dataType: 'html', @@ -217,7 +217,7 @@ var suggestionUrl = $(searchField).attr('data-search-url').substr(1,$(searchField).attr('data-search-url').length-2); $.ajax({ headers: { - "X-Get-Fragment" : 'Partial' + "X-Pjax" : 'Partial' }, type: "GET", url: suggestionUrl+'/'+request.term, diff --git a/tests/control/PjaxResponseNegotiatorTest.php b/tests/control/PjaxResponseNegotiatorTest.php new file mode 100644 index 000000000..76ac8df93 --- /dev/null +++ b/tests/control/PjaxResponseNegotiatorTest.php @@ -0,0 +1,22 @@ + function() {return 'default response';}, + )); + $request = new SS_HTTPRequest('GET', '/'); // not setting pjax header + $this->assertEquals('default response', $negotiator->respond($request)); + } + + function testSelectsFragmentByHeader() { + $negotiator = new PjaxResponseNegotiator(array( + 'default' => function() {return 'default response';}, + 'myfragment' => function() {return 'myfragment response';}, + )); + $request = new SS_HTTPRequest('GET', '/'); + $request->addHeader('X-Pjax', 'myfragment'); + $this->assertEquals('myfragment response', $negotiator->respond($request)); + } + +} \ No newline at end of file From aebbb10c9f65a67497e3ea5b3ab0256d3c5ebba0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 5 Apr 2012 22:13:56 +0200 Subject: [PATCH 43/44] MINOR Skip processing in CMS on empty ajax responses, as they're usually a pseudo redirect (via X-ControllerURL) --- admin/javascript/LeftAndMain.Content.js | 6 ++++-- admin/javascript/LeftAndMain.js | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/admin/javascript/LeftAndMain.Content.js b/admin/javascript/LeftAndMain.Content.js index 16e80272d..1979ed701 100644 --- a/admin/javascript/LeftAndMain.Content.js +++ b/admin/javascript/LeftAndMain.Content.js @@ -83,11 +83,11 @@ * (XMLHTTPRequest) xmlhttp */ loadForm_responseHandler: function(oldForm, html, status, xmlhttp) { + if(!html) return; if(oldForm.length > 0) { oldForm.replaceWith(html); // triggers onmatch() on form - } - else { + } else { $('.cms-content').append(html); } @@ -199,6 +199,8 @@ */ submitForm_responseHandler: function(oldForm, data, status, xmlhttp, origData) { if(status == 'success') { + if(!data) return; + var form, newContent = $(data); // HACK If response contains toplevel panel rather than a form, replace it instead. diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 6c989144e..1196371e7 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -227,6 +227,10 @@ jQuery.noConflict(); headers: headers, url: state.url, success: function(data, status, xhr) { + // Pseudo-redirects via X-ControllerURL might return empty data, in which + // case we'll ignore the response + if(!data) return; + // Update title var title = xhr.getResponseHeader('X-Title'); if(title) document.title = title; From 3ae0ac780509f11b9c6b548e41578b48eecb053c Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 28 Mar 2012 01:43:16 +0200 Subject: [PATCH 44/44] ENHANCEMENT: Rightlick submenu styles and ability to add page with pagetype --- admin/code/LeftAndMain.php | 2 +- admin/css/screen.css | 94 ++++++++++++++------------ admin/images/btn-icon-sc495ceeeca.png | Bin 17782 -> 0 bytes admin/scss/_tree.scss | 78 ++++++++++++++------- 4 files changed, 103 insertions(+), 71 deletions(-) delete mode 100644 admin/images/btn-icon-sc495ceeeca.png diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 9d6af1e39..20b652fa9 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -613,7 +613,7 @@ class LeftAndMain extends Controller implements PermissionProvider { // getChildrenAsUL is a flexible and complex way of traversing the tree $titleEval = ' - "
  • ID\" data-id=\"$child->ID\" class=\"" . $child->CMSTreeClasses($extraArg) . "\">" . + "
  • ID\" data-id=\"$child->ID\" data-ssclass=\"$child->ClassName\" class=\"" . $child->CMSTreeClasses($extraArg) . "\">" . " " . "Link("show"), $child->ID) . "\" title=\"' . _t('LeftAndMain.PAGETYPE','Page type: ') diff --git a/admin/css/screen.css b/admin/css/screen.css index 613a35646..68cd69c77 100644 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -35,49 +35,49 @@ If more variables exist in the future, consider creating a variables file.*/ /** ---------------------------------------------------- Double tone borders http://daverupert.com/2011/06/two-tone-borders-with-css3/ ----------------------------------------------------- */ /** ----------------------------- Sprite images ----------------------------- */ /** Helper SCSS file for generating sprites for the interface. */ -.btn-icon-sprite, .ui-state-default .btn-icon-accept, .ui-state-default .btn-icon-accept_disabled, .ui-state-default .btn-icon-add, .ui-state-default .btn-icon-add_disabled, .ui-state-default .btn-icon-addpage, .ui-state-default .btn-icon-addpage_disabled, .ui-state-default .btn-icon-arrow-circle-135-left, .ui-state-default .btn-icon-back, .ui-state-default .btn-icon-back_disabled, .ui-state-default .btn-icon-chain--arrow, .ui-state-default .btn-icon-chain--exclamation, .ui-state-default .btn-icon-chain--minus, .ui-state-default .btn-icon-chain--pencil, .ui-state-default .btn-icon-chain--plus, .ui-state-default .btn-icon-chain-small, .ui-state-default .btn-icon-chain-unchain, .ui-state-default .btn-icon-chain, .ui-state-default .btn-icon-cross-circle, .ui-state-default .btn-icon-cross-circle_disabled, .ui-state-default .btn-icon-decline, .ui-state-default .btn-icon-decline_disabled, .ui-state-default .btn-icon-download-csv, .ui-state-default .btn-icon-drive-upload, .ui-state-default .btn-icon-drive-upload_disabled, .ui-state-default .btn-icon-magnifier, .ui-state-default .btn-icon-minus-circle, .ui-state-default .btn-icon-minus-circle_disabled, .ui-state-default .btn-icon-navigation, .ui-state-default .btn-icon-navigation_disabled, .ui-state-default .btn-icon-network-cloud, .ui-state-default .btn-icon-network-cloud_disabled, .ui-state-default .btn-icon-pencil, .ui-state-default .btn-icon-pencil_disabled, .ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-state-default .btn-icon-preview, .ui-state-default .btn-icon-preview_disabled, .ui-state-default .btn-icon-settings, .ui-state-default .btn-icon-settings_disabled, .ui-state-default .btn-icon-unpublish, .ui-state-default .btn-icon-unpublish_disabled { background: url('../images/btn-icon-sc495ceeeca.png') no-repeat; } +.btn-icon-sprite, .ui-state-default .btn-icon-accept, .ui-state-default .btn-icon-accept_disabled, .ui-state-default .btn-icon-add, .ui-state-default .btn-icon-add_disabled, .ui-state-default .btn-icon-addpage, .ui-state-default .btn-icon-addpage_disabled, .ui-state-default .btn-icon-arrow-circle-135-left, .ui-state-default .btn-icon-back, .ui-state-default .btn-icon-back_disabled, .ui-state-default .btn-icon-chain--arrow, .ui-state-default .btn-icon-chain--exclamation, .ui-state-default .btn-icon-chain--minus, .ui-state-default .btn-icon-chain--pencil, .ui-state-default .btn-icon-chain--plus, .ui-state-default .btn-icon-chain-small, .ui-state-default .btn-icon-chain-unchain, .ui-state-default .btn-icon-chain, .ui-state-default .btn-icon-cross-circle, .ui-state-default .btn-icon-cross-circle_disabled, .ui-state-default .btn-icon-decline, .ui-state-default .btn-icon-decline_disabled, .ui-state-default .btn-icon-download-csv, .ui-state-default .btn-icon-drive-upload, .ui-state-default .btn-icon-drive-upload_disabled, .ui-state-default .btn-icon-magnifier, .ui-state-default .btn-icon-minus-circle, .ui-state-default .btn-icon-minus-circle_disabled, .ui-state-default .btn-icon-navigation, .ui-state-default .btn-icon-navigation_disabled, .ui-state-default .btn-icon-network-cloud, .ui-state-default .btn-icon-network-cloud_disabled, .ui-state-default .btn-icon-pencil, .ui-state-default .btn-icon-pencil_disabled, .ui-state-default .btn-icon-plug-disconnect-prohibition, .ui-state-default .btn-icon-plug-disconnect-prohibition_disabled, .ui-state-default .btn-icon-preview, .ui-state-default .btn-icon-preview_disabled, .ui-state-default .btn-icon-settings, .ui-state-default .btn-icon-settings_disabled, .ui-state-default .btn-icon-unpublish, .ui-state-default .btn-icon-unpublish_disabled { background: url('../images/btn-icon-s41050dc384.png') no-repeat; } .ui-state-default .btn-icon-accept { background-position: 0 0; } .ui-state-default .btn-icon-accept_disabled { background-position: 0 -17px; } .ui-state-default .btn-icon-add { background-position: 0 -34px; } .ui-state-default .btn-icon-add_disabled { background-position: 0 -52px; } .ui-state-default .btn-icon-addpage { background-position: 0 -70px; } -.ui-state-default .btn-icon-addpage_disabled { background-position: 0 -88px; } -.ui-state-default .btn-icon-arrow-circle-135-left { background-position: 0 -104px; } -.ui-state-default .btn-icon-back { background-position: 0 -120px; } -.ui-state-default .btn-icon-back_disabled { background-position: 0 -136px; } -.ui-state-default .btn-icon-chain--arrow { background-position: 0 -151px; } -.ui-state-default .btn-icon-chain--exclamation { background-position: 0 -167px; } -.ui-state-default .btn-icon-chain--minus { background-position: 0 -183px; } -.ui-state-default .btn-icon-chain--pencil { background-position: 0 -199px; } -.ui-state-default .btn-icon-chain--plus { background-position: 0 -215px; } -.ui-state-default .btn-icon-chain-small { background-position: 0 -231px; } -.ui-state-default .btn-icon-chain-unchain { background-position: 0 -247px; } -.ui-state-default .btn-icon-chain { background-position: 0 -263px; } -.ui-state-default .btn-icon-cross-circle { background-position: 0 -279px; } -.ui-state-default .btn-icon-cross-circle_disabled { background-position: 0 -295px; } -.ui-state-default .btn-icon-decline { background-position: 0 -311px; } -.ui-state-default .btn-icon-decline_disabled { background-position: 0 -328px; } -.ui-state-default .btn-icon-download-csv { background-position: 0 -345px; } -.ui-state-default .btn-icon-drive-upload { background-position: 0 -363px; } -.ui-state-default .btn-icon-drive-upload_disabled { background-position: 0 -379px; } -.ui-state-default .btn-icon-magnifier { background-position: 0 -395px; } -.ui-state-default .btn-icon-minus-circle { background-position: 0 -411px; } -.ui-state-default .btn-icon-minus-circle_disabled { background-position: 0 -427px; } -.ui-state-default .btn-icon-navigation { background-position: 0 -443px; } -.ui-state-default .btn-icon-navigation_disabled { background-position: 0 -459px; } -.ui-state-default .btn-icon-network-cloud { background-position: 0 -475px; } -.ui-state-default .btn-icon-network-cloud_disabled { background-position: 0 -491px; } -.ui-state-default .btn-icon-pencil { background-position: 0 -507px; } -.ui-state-default .btn-icon-pencil_disabled { background-position: 0 -523px; } -.ui-state-default .btn-icon-plug-disconnect-prohibition { background-position: 0 -539px; } -.ui-state-default .btn-icon-plug-disconnect-prohibition_disabled { background-position: 0 -555px; } -.ui-state-default .btn-icon-preview { background-position: 0 -571px; } -.ui-state-default .btn-icon-preview_disabled { background-position: 0 -588px; } -.ui-state-default .btn-icon-settings { background-position: 0 -605px; } -.ui-state-default .btn-icon-settings_disabled { background-position: 0 -621px; } -.ui-state-default .btn-icon-unpublish { background-position: 0 -637px; } -.ui-state-default .btn-icon-unpublish_disabled { background-position: 0 -655px; } +.ui-state-default .btn-icon-addpage_disabled { background-position: 0 -86px; } +.ui-state-default .btn-icon-arrow-circle-135-left { background-position: 0 -102px; } +.ui-state-default .btn-icon-back { background-position: 0 -118px; } +.ui-state-default .btn-icon-back_disabled { background-position: 0 -134px; } +.ui-state-default .btn-icon-chain--arrow { background-position: 0 -149px; } +.ui-state-default .btn-icon-chain--exclamation { background-position: 0 -165px; } +.ui-state-default .btn-icon-chain--minus { background-position: 0 -181px; } +.ui-state-default .btn-icon-chain--pencil { background-position: 0 -197px; } +.ui-state-default .btn-icon-chain--plus { background-position: 0 -213px; } +.ui-state-default .btn-icon-chain-small { background-position: 0 -229px; } +.ui-state-default .btn-icon-chain-unchain { background-position: 0 -245px; } +.ui-state-default .btn-icon-chain { background-position: 0 -261px; } +.ui-state-default .btn-icon-cross-circle { background-position: 0 -277px; } +.ui-state-default .btn-icon-cross-circle_disabled { background-position: 0 -293px; } +.ui-state-default .btn-icon-decline { background-position: 0 -309px; } +.ui-state-default .btn-icon-decline_disabled { background-position: 0 -326px; } +.ui-state-default .btn-icon-download-csv { background-position: 0 -343px; } +.ui-state-default .btn-icon-drive-upload { background-position: 0 -361px; } +.ui-state-default .btn-icon-drive-upload_disabled { background-position: 0 -377px; } +.ui-state-default .btn-icon-magnifier { background-position: 0 -393px; } +.ui-state-default .btn-icon-minus-circle { background-position: 0 -409px; } +.ui-state-default .btn-icon-minus-circle_disabled { background-position: 0 -425px; } +.ui-state-default .btn-icon-navigation { background-position: 0 -441px; } +.ui-state-default .btn-icon-navigation_disabled { background-position: 0 -457px; } +.ui-state-default .btn-icon-network-cloud { background-position: 0 -473px; } +.ui-state-default .btn-icon-network-cloud_disabled { background-position: 0 -489px; } +.ui-state-default .btn-icon-pencil { background-position: 0 -505px; } +.ui-state-default .btn-icon-pencil_disabled { background-position: 0 -521px; } +.ui-state-default .btn-icon-plug-disconnect-prohibition { background-position: 0 -537px; } +.ui-state-default .btn-icon-plug-disconnect-prohibition_disabled { background-position: 0 -553px; } +.ui-state-default .btn-icon-preview { background-position: 0 -569px; } +.ui-state-default .btn-icon-preview_disabled { background-position: 0 -586px; } +.ui-state-default .btn-icon-settings { background-position: 0 -603px; } +.ui-state-default .btn-icon-settings_disabled { background-position: 0 -619px; } +.ui-state-default .btn-icon-unpublish { background-position: 0 -635px; } +.ui-state-default .btn-icon-unpublish_disabled { background-position: 0 -653px; } .icon { text-indent: -9999px; border: none; outline: none; } .icon.icon-24 { width: 24px; height: 24px; background: url('../images/menu-icons/24x24-s546fcae8fd.png'); } @@ -550,14 +550,18 @@ form.import-form label.left { width: 250px; } .cms .jstree-rtl > ul > li, .TreeDropdownField .treedropdownfield-panel .jstree-rtl > ul > li { margin-right: 0px; } .cms .jstree > ul > li, .TreeDropdownField .treedropdownfield-panel .jstree > ul > li { margin-left: 0px; } .cms #vakata-dragged, .TreeDropdownField .treedropdownfield-panel #vakata-dragged { display: block; margin: 0 0 0 0; padding: 4px 4px 4px 24px; position: absolute; top: -2000px; line-height: 16px; z-index: 10000; } -.cms #vakata-contextmenu, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu { display: block; visibility: hidden; left: 0; top: -200px; position: absolute; margin: 0; padding: 0; min-width: 180px; background: #ebebeb; border: 1px solid silver; z-index: 10000; *width: 180px; } +.cms #vakata-contextmenu, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu { display: block; visibility: hidden; left: 0; top: -200px; position: absolute; margin: 0; padding: 0; min-width: 180px; background: #FFF; border: 1px solid silver; z-index: 10000; *width: 180px; -moz-box-shadow: 0 0 10px #cccccc; -webkit-box-shadow: 0 0 10px #cccccc; -o-box-shadow: 0 0 10px #cccccc; box-shadow: 0 0 10px #cccccc; } +.cms #vakata-contextmenu::before, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu::before { content: ""; display: block; /* reduce the damage in FF3.0 */ position: absolute; top: -10px; left: 24px; width: 0; border-width: 0 6px 10px 6px; border-color: #FFF transparent; border-style: solid; z-index: 10000; } +.cms #vakata-contextmenu::after, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu::after { content: ""; display: block; /* reduce the damage in FF3.0 */ position: absolute; top: -11px; left: 23px; width: 0; border-width: 0 7px 11px 7px; border-color: #CCC transparent; border-style: solid; } .cms #vakata-contextmenu ul, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu ul { min-width: 180px; *width: 180px; } .cms #vakata-contextmenu ul, .cms #vakata-contextmenu li, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu ul, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li { margin: 0; padding: 0; list-style-type: none; display: block; } -.cms #vakata-contextmenu li, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li { line-height: 20px; min-height: 20px; position: relative; padding: 0px; } -.cms #vakata-contextmenu li a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li a { padding: 1px 6px; line-height: 17px; display: block; text-decoration: none; margin: 1px 1px 0 1px; } -.cms #vakata-contextmenu li ins, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li ins { float: left; width: 16px; height: 16px; text-decoration: none; margin-right: 2px; } -.cms #vakata-contextmenu li a:hover, .cms #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li a:hover, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li.vakata-hover > a { background: gray; color: white; } -.cms #vakata-contextmenu li ul, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li ul { display: none; position: absolute; top: -2px; left: 100%; background: #ebebeb; border: 1px solid gray; } +.cms #vakata-contextmenu li, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li { line-height: 20px; min-height: 23px; position: relative; padding: 0px; } +.cms #vakata-contextmenu li:last-child, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li:last-child { margin-bottom: 1px; } +.cms #vakata-contextmenu li a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li a { padding: 1px 10px; line-height: 23px; display: block; text-decoration: none; margin: 1px 1px 0 1px; border: 0; } +.cms #vakata-contextmenu li ins, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li ins { float: left; width: 0; height: 0; text-decoration: none; margin-right: 2px; } +.cms #vakata-contextmenu li .jstree-pageicon, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li .jstree-pageicon { margin-top: 3px; margin-right: 5px; } +.cms #vakata-contextmenu li a:hover, .cms #vakata-contextmenu li.vakata-hover > a, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li a:hover, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li.vakata-hover > a { padding: 1px 10px; background: #3875d7; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIyMCUiIHN0b3AtY29sb3I9IiMzODc1ZDciLz48c3RvcCBvZmZzZXQ9IjkwJSIgc3RvcC1jb2xvcj0iIzJhNjJiYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); background-image: -webkit-linear-gradient(top, #3875d7 20%, #2a62bc 90%); background-image: -moz-linear-gradient(top, #3875d7 20%, #2a62bc 90%); background-image: -o-linear-gradient(top, #3875d7 20%, #2a62bc 90%); background-image: -ms-linear-gradient(top, #3875d7 20%, #2a62bc 90%); background-image: linear-gradient(top, #3875d7 20%, #2a62bc 90%); color: #FFF; border: none; } +.cms #vakata-contextmenu li ul, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li ul { display: none; position: absolute; top: -2px; left: 100%; background: #FFF; border: 1px solid silver; -moz-box-shadow: 0 0 10px #cccccc; -webkit-box-shadow: 0 0 10px #cccccc; -o-box-shadow: 0 0 10px #cccccc; box-shadow: 0 0 10px #cccccc; } .cms #vakata-contextmenu .right, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .right { right: 100%; left: auto; } .cms #vakata-contextmenu .bottom, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu .bottom { bottom: -1px; top: auto; } .cms #vakata-contextmenu li.vakata-separator, .TreeDropdownField .treedropdownfield-panel #vakata-contextmenu li.vakata-separator { min-height: 0; height: 1px; line-height: 1px; font-size: 1px; overflow: hidden; margin: 0 2px; background: silver; /* border-top:1px solid #fefefe; */ padding: 0; } @@ -622,8 +626,8 @@ form.import-form label.left { width: 250px; } .tree-holder.jstree-apple li, .cms-tree.jstree-apple li { padding: 0px; clear: left; } .tree-holder.jstree-apple ins, .cms-tree.jstree-apple ins { background-color: transparent; background-image: url(../images/sitetree_ss_default_icons.png); } .tree-holder.jstree-apple li.jstree-checked > a, .tree-holder.jstree-apple li.jstree-checked > a:link, .cms-tree.jstree-apple li.jstree-checked > a, .cms-tree.jstree-apple li.jstree-checked > a:link { background-color: #efe999; } -.tree-holder.jstree-apple .jstree-closed > ins, .cms-tree.jstree-apple .jstree-closed > ins { background-position: 0 0; cursor: pointer; } -.tree-holder.jstree-apple .jstree-open > ins, .cms-tree.jstree-apple .jstree-open > ins { background-position: -20px 0; cursor: pointer; } +.tree-holder.jstree-apple .jstree-closed > ins, .cms-tree.jstree-apple .jstree-closed > ins { background-position: 0 0; } +.tree-holder.jstree-apple .jstree-open > ins, .cms-tree.jstree-apple .jstree-open > ins { background-position: -20px 0; } a .jstree-pageicon { display: block; float: left; width: 16px; height: 16px; margin-right: 4px; background-color: transparent; background-image: url(../images/sitetree_ss_pageclass_icons_default.png); background-repeat: no-repeat; } diff --git a/admin/images/btn-icon-sc495ceeeca.png b/admin/images/btn-icon-sc495ceeeca.png deleted file mode 100644 index 7f27b0297d708ca06c8cb04c3c4bd9d3fdc14d3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17782 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 a { - background: gray; - color: white; + padding: 1px 10px; + background: #3875d7; + @include background-image(linear-gradient(top, #3875d7 20%, #2a62bc 90%)); + color: #FFF; + border: none; } #vakata-contextmenu li ul { display: none; position: absolute; top: -2px; left: 100%; - background: #ebebeb; - border: 1px solid gray; + background: #FFF; + border: 1px solid silver; + @include box-shadow(0 0 10px #CCC); } #vakata-contextmenu .right { right: 100%; @@ -246,7 +276,7 @@ } li.jstree-open > ul { display: block; - + margin-left:-13px; li ul { margin-left:2px; } @@ -476,7 +506,7 @@ border: 1px solid #7C8816; background-color: #DAE79A; } - + /* comment speech bubble - ccs3 only - source: http://nicolasgallagher.com/pure-css-speech-bubbles/demo/ */ & span.comment-count { clear: both; @@ -541,13 +571,11 @@ } & .jstree-closed > ins { - background-position:0 0; - cursor:pointer; + background-position:0 0; } & .jstree-open > ins { - background-position:-20px 0; - cursor:pointer; + background-position:-20px 0; } } @@ -594,4 +622,4 @@ li.class-ErrorPage > a .jstree-pageicon { } } } -} +} \ No newline at end of file