From fc349db511c003d4b08147f0bf4ed2904d5b304d Mon Sep 17 00:00:00 2001 From: GuySartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:01:17 +1300 Subject: [PATCH 01/11] API Add a way to check if a form or form field has an extra css class (#10112) Required for silverstripe/silverstripe-admin#1252 --- src/Forms/Form.php | 19 +++++++++++++++++++ src/Forms/FormField.php | 19 +++++++++++++++++++ tests/php/Forms/FormFieldTest.php | 13 +++++++++++++ tests/php/Forms/FormTest.php | 13 +++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/Forms/Form.php b/src/Forms/Form.php index c35561d23..9a6fd11d1 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -1779,6 +1779,25 @@ class Form extends ViewableData implements HasRequestHandler return implode(' ', array_unique($this->extraClasses)); } + /** + * Check if a CSS-class has been added to the form container. + * + * @param string $class A string containing a classname or several class + * names delimited by a single space. + * @return boolean True if all of the classnames passed in have been added. + */ + public function hasExtraClass($class) + { + //split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + if (!isset($this->extraClasses[$class])) { + return false; + } + } + return true; + } + /** * Add a CSS-class to the form-container. If needed, multiple classes can * be added by delimiting a string with spaces. diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index e4663291c..98f25b735 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -602,6 +602,25 @@ class FormField extends RequestHandler return implode(' ', $classes); } + /** + * Check if a CSS-class has been added to the form container. + * + * @param string $class A string containing a classname or several class + * names delimited by a single space. + * @return boolean True if all of the classnames passed in have been added. + */ + public function hasExtraClass($class) + { + //split at white space + $classes = preg_split('/\s+/', $class); + foreach ($classes as $class) { + if (!isset($this->extraClasses[$class])) { + return false; + } + } + return true; + } + /** * Add one or more CSS-classes to the FormField container. * diff --git a/tests/php/Forms/FormFieldTest.php b/tests/php/Forms/FormFieldTest.php index 24d59fee9..411d81e1d 100644 --- a/tests/php/Forms/FormFieldTest.php +++ b/tests/php/Forms/FormFieldTest.php @@ -93,6 +93,19 @@ class FormFieldTest extends SapphireTest $this->assertStringEndsWith('class1 class2', $field->extraClass()); } + public function testHasExtraClass() + { + $field = new FormField('MyField'); + $field->addExtraClass('class1'); + $field->addExtraClass('class2'); + $this->assertTrue($field->hasExtraClass('class1')); + $this->assertTrue($field->hasExtraClass('class2')); + $this->assertTrue($field->hasExtraClass('class1 class2')); + $this->assertTrue($field->hasExtraClass('class2 class1')); + $this->assertFalse($field->hasExtraClass('class3')); + $this->assertFalse($field->hasExtraClass('class2 class3')); + } + public function testRemoveExtraClass() { $field = new FormField('MyField'); diff --git a/tests/php/Forms/FormTest.php b/tests/php/Forms/FormTest.php index f9fb8e51d..6e2ba55ee 100644 --- a/tests/php/Forms/FormTest.php +++ b/tests/php/Forms/FormTest.php @@ -726,6 +726,19 @@ class FormTest extends FunctionalTest $this->assertStringEndsWith('class1 class2', $form->extraClass()); } + public function testHasExtraClass() + { + $form = $this->getStubForm(); + $form->addExtraClass('class1'); + $form->addExtraClass('class2'); + $this->assertTrue($form->hasExtraClass('class1')); + $this->assertTrue($form->hasExtraClass('class2')); + $this->assertTrue($form->hasExtraClass('class1 class2')); + $this->assertTrue($form->hasExtraClass('class2 class1')); + $this->assertFalse($form->hasExtraClass('class3')); + $this->assertFalse($form->hasExtraClass('class2 class3')); + } + public function testRemoveExtraClass() { $form = $this->getStubForm(); From b8d37f9ae43b971890ae311d8bdcfaf5c32927ae Mon Sep 17 00:00:00 2001 From: Kirk Mayo Date: Wed, 3 Nov 2021 14:26:16 +1300 Subject: [PATCH 02/11] NEW Validate the Title on Group is not empty (#10113) --- lang/en.yml | 1 + src/Dev/SapphireTest.php | 12 +++- src/Security/Group.php | 49 ++++++++++++++- tests/php/Security/GroupCsvBulkLoaderTest.php | 2 +- tests/php/Security/GroupTest.php | 60 ++++++++++++++++++- tests/php/Security/GroupTest.yml | 4 ++ 6 files changed, 122 insertions(+), 6 deletions(-) diff --git a/lang/en.yml b/lang/en.yml index 849dad055..7fb0e9ab6 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -217,6 +217,7 @@ en: GROUPNAME: 'Group name' GroupReminder: 'If you choose a parent group, this group will take all it''s roles' HierarchyPermsError: 'Can''t assign parent group "{group}" with privileged permissions (requires ADMIN access)' + ValidationIdentifierAlreadyExists: 'A Group ({group}) already exists with the same {identifier}' Locked: 'Locked?' MEMBERS: Members NEWGROUP: 'New Group' diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 2a0d24622..c46a650a7 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -2434,9 +2434,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly $member = $this->cache_generatedMembers[$permCode]; } else { // Generate group with these permissions - $group = Group::create(); - $group->Title = "$permCode group"; - $group->write(); + $group = Group::get()->filterAny([ + 'Code' => "$permCode-group", + 'Title' => "$permCode group", + ])->first(); + if (!$group || !$group->exists()) { + $group = Group::create(); + $group->Title = "$permCode group"; + $group->write(); + } // Create each individual permission foreach ($permArray as $permArrayItem) { diff --git a/src/Security/Group.php b/src/Security/Group.php index 48636f452..958467b4d 100755 --- a/src/Security/Group.php +++ b/src/Security/Group.php @@ -4,6 +4,7 @@ namespace SilverStripe\Security; use SilverStripe\Admin\SecurityAdmin; use SilverStripe\Core\Convert; +use SilverStripe\Forms\CompositeValidator; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; @@ -21,6 +22,7 @@ use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\LiteralField; +use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\Tab; use SilverStripe\Forms\TabSet; use SilverStripe\Forms\TextareaField; @@ -480,7 +482,16 @@ class Group extends DataObject */ public function setCode($val) { - $this->setField("Code", Convert::raw2url($val)); + $currentGroups = Group::get() + ->map('Code', 'Title') + ->toArray(); + $code = Convert::raw2url($val); + $count = 2; + while (isset($currentGroups[$code])) { + $code = Convert::raw2url($val . '-' . $count); + $count++; + } + $this->setField("Code", $code); } public function validate() @@ -506,9 +517,45 @@ class Group extends DataObject } } + $currentGroups = Group::get() + ->filter('ID:not', $this->ID) + ->map('Code', 'Title') + ->toArray(); + + if (isset($currentGroups[$this->Code])) { + $result->addError( + _t( + 'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists', + 'A Group ({group}) already exists with the same {identifier}', + ['group' => $this->Code, 'identifier' => 'Code'] + ) + ); + } + + if (in_array($this->Title, $currentGroups)) { + $result->addError( + _t( + 'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists', + 'A Group ({group}) already exists with the same {identifier}', + ['group' => $this->Title, 'identifier' => 'Title'] + ) + ); + } + return $result; } + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + + $validator->addValidator(RequiredFields::create([ + 'Title' + ])); + + return $validator; + } + public function onBeforeWrite() { parent::onBeforeWrite(); diff --git a/tests/php/Security/GroupCsvBulkLoaderTest.php b/tests/php/Security/GroupCsvBulkLoaderTest.php index 5bb41a93c..531bc33e9 100644 --- a/tests/php/Security/GroupCsvBulkLoaderTest.php +++ b/tests/php/Security/GroupCsvBulkLoaderTest.php @@ -38,7 +38,7 @@ class GroupCsvBulkLoaderTest extends SapphireTest $updated = $results->Updated()->toArray(); $this->assertEquals(count($updated), 1); - $this->assertEquals($updated[0]->Code, 'newgroup1'); + $this->assertEquals($updated[0]->Code, 'newgroup1-2'); $this->assertEquals($updated[0]->Title, 'New Group 1'); } diff --git a/tests/php/Security/GroupTest.php b/tests/php/Security/GroupTest.php index 79d446311..b36b9a9b8 100644 --- a/tests/php/Security/GroupTest.php +++ b/tests/php/Security/GroupTest.php @@ -5,6 +5,7 @@ namespace SilverStripe\Security\Tests; use InvalidArgumentException; use SilverStripe\Control\Controller; use SilverStripe\Dev\FunctionalTest; +use SilverStripe\Forms\RequiredFields; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Group; @@ -33,7 +34,7 @@ class GroupTest extends FunctionalTest $this->assertEquals('my-title', $g1->Code, 'Custom title gets converted to code if none exists already'); $g2 = new Group(); - $g2->Title = "My Title"; + $g2->Title = "My Title and Code"; $g2->Code = "my-code"; $g2->write(); $this->assertEquals('my-code', $g2->Code, 'Custom attributes are not overwritten by Title field'); @@ -101,6 +102,7 @@ class GroupTest extends FunctionalTest { $member = $this->objFromFixture(TestMember::class, 'admin'); $group = new Group(); + $group->Title = 'Title'; // Can save user to unsaved group $group->Members()->add($member); @@ -121,6 +123,7 @@ class GroupTest extends FunctionalTest /** @var Group $childGroup */ $childGroup = $this->objFromFixture(Group::class, 'childgroup'); $orphanGroup = new Group(); + $orphanGroup->Title = 'Title'; $orphanGroup->ParentID = 99999; $orphanGroup->write(); @@ -280,4 +283,59 @@ class GroupTest extends FunctionalTest 'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission' ); } + + public function testGroupTitleValidation() + { + $group1 = $this->objFromFixture(Group::class, 'group1'); + + $newGroup = new Group(); + + $validators = $newGroup->getCMSCompositeValidator()->getValidators(); + $this->assertCount(1, $validators); + $this->assertInstanceOf(RequiredFields::class, $validators[0]); + $this->assertTrue(in_array('Title', $validators[0]->getRequired())); + + $newGroup->Title = $group1->Title; + $result = $newGroup->validate(); + $this->assertFalse( + $result->isValid(), + 'Group names cannot be duplicated' + ); + + $newGroup->Title = 'Title'; + $result = $newGroup->validate(); + $this->assertTrue($result->isValid()); + } + + public function testGroupTitleDuplication() + { + $group = $this->objFromFixture(Group::class, 'group1'); + $group->Title = 'Group title modified'; + $group->write(); + $this->assertEquals('group-1', $group->Code); + + $group = new Group(); + $group->Title = 'Group 1'; + $group->write(); + $this->assertEquals('group-1-2', $group->Code); + + $group = new Group(); + $group->Title = 'Duplicate'; + $group->write(); + $group->Title = 'Duplicate renamed'; + $group->write(); + $this->assertEquals('duplicate', $group->Code); + + $group = new Group(); + $group->Title = 'Duplicate'; + $group->write(); + $group->Title = 'More renaming'; + $group->write(); + $this->assertEquals('duplicate-2', $group->Code); + + $group = new Group(); + $group->Title = 'Duplicate'; + $group->write(); + $this->assertEquals('duplicate-3', $group->Code); + } } diff --git a/tests/php/Security/GroupTest.yml b/tests/php/Security/GroupTest.yml index b5dd61595..19975ff31 100644 --- a/tests/php/Security/GroupTest.yml +++ b/tests/php/Security/GroupTest.yml @@ -1,12 +1,16 @@ 'SilverStripe\Security\Group': admingroup: + Title: Admin Group Code: admingroup parentgroup: + Title: Parent Group Code: parentgroup childgroup: + Title: Child Group Code: childgroup Parent: '=>SilverStripe\Security\Group.parentgroup' grandchildgroup: + Title: Grandchild Group Code: grandchildgroup Parent: '=>SilverStripe\Security\Group.childgroup' group1: From d6866af7e590c6ae0437a605fcb0ab523f27b114 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Thu, 4 Nov 2021 10:53:42 +0000 Subject: [PATCH 03/11] Fix broken tests --- src/Dev/SapphireTest.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index c46a650a7..bca1c2f82 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -1108,9 +1108,15 @@ if (class_exists(IsEqualCanonicalizing::class)) { $member = $this->cache_generatedMembers[$permCode]; } else { // Generate group with these permissions - $group = Group::create(); - $group->Title = "$permCode group"; - $group->write(); + $group = Group::get()->filterAny([ + 'Code' => "$permCode-group", + 'Title' => "$permCode group", + ])->first(); + if (!$group || !$group->exists()) { + $group = Group::create(); + $group->Title = "$permCode group"; + $group->write(); + } // Create each individual permission foreach ($permArray as $permArrayItem) { From e53c18528cadb8954d2c6808f15b936c0cb21717 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Fri, 5 Nov 2021 12:06:55 +1300 Subject: [PATCH 04/11] MNT Remove polyfill --- composer.json | 1 - tests/php/Forms/TreeMultiselectFieldTest.php | 153 +++++++------------ 2 files changed, 51 insertions(+), 103 deletions(-) diff --git a/composer.json b/composer.json index d803aac07..88e327f59 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,6 @@ }, "require-dev": { "phpunit/phpunit": "^9.5", - "dms/phpunit-arraysubset-asserts": "^0.3.0", "silverstripe/versioned": "^1", "squizlabs/php_codesniffer": "^3.5" }, diff --git a/tests/php/Forms/TreeMultiselectFieldTest.php b/tests/php/Forms/TreeMultiselectFieldTest.php index f004aea5e..e1f5354a2 100644 --- a/tests/php/Forms/TreeMultiselectFieldTest.php +++ b/tests/php/Forms/TreeMultiselectFieldTest.php @@ -2,7 +2,6 @@ namespace SilverStripe\Forms\Tests; -use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use SilverStripe\Assets\File; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\Form; @@ -13,8 +12,6 @@ use SilverStripe\View\SSViewer; class TreeMultiselectFieldTest extends SapphireTest { - use ArraySubsetAsserts; - protected static $fixture_file = 'TreeDropdownFieldTest.yml'; protected static $extra_dataobjects = [ @@ -136,38 +133,24 @@ class TreeMultiselectFieldTest extends SapphireTest $this->assertEquals($fieldId, sprintf('%s_%s', $this->formId, $this->fieldName)); $schemaStateDefaults = $field->getSchemaStateDefaults(); - $this->assertArraySubset( - [ - 'id' => $fieldId, - 'name' => $this->fieldName, - 'value' => 'unchanged' - ], - $schemaStateDefaults, - true - ); + $this->assertSame($fieldId, $schemaStateDefaults['id']); + $this->assertSame($this->fieldName, $schemaStateDefaults['name']); + $this->assertSame('unchanged', $schemaStateDefaults['value']); $schemaDataDefaults = $field->getSchemaDataDefaults(); - $this->assertArraySubset( - [ - 'id' => $fieldId, - 'name' => $this->fieldName, - 'type' => 'text', - 'schemaType' => 'SingleSelect', - 'component' => 'TreeDropdownField', - 'holderId' => sprintf('%s_Holder', $fieldId), - 'title' => 'Test tree', - 'extraClass' => 'treemultiselect multiple searchable', - 'data' => [ - 'urlTree' => 'field/TestTree/tree', - 'showSearch' => true, - 'emptyString' => '(Search or choose File)', - 'hasEmptyDefault' => false, - 'multiple' => true - ] - ], - $schemaDataDefaults, - true - ); + $this->assertSame($fieldId, $schemaDataDefaults['id']); + $this->assertSame($this->fieldName, $schemaDataDefaults['name']); + $this->assertSame('text', $schemaDataDefaults['type']); + $this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']); + $this->assertSame('TreeDropdownField', $schemaDataDefaults['component']); + $this->assertSame(sprintf('%s_Holder', $fieldId), $schemaDataDefaults['holderId']); + $this->assertSame('Test tree', $schemaDataDefaults['title']); + $this->assertSame('treemultiselect multiple searchable', $schemaDataDefaults['extraClass']); + $this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']); + $this->assertSame(true, $schemaDataDefaults['data']['showSearch']); + $this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']); + $this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']); + $this->assertSame(true, $schemaDataDefaults['data']['multiple']); $items = $field->getItems(); $this->assertCount(0, $items, 'there must be no items selected'); @@ -188,15 +171,9 @@ class TreeMultiselectFieldTest extends SapphireTest $field->setValue($this->fieldValue); $schemaStateDefaults = $field->getSchemaStateDefaults(); - $this->assertArraySubset( - [ - 'id' => $field->ID(), - 'name' => 'TestTree', - 'value' => $this->folderIds - ], - $schemaStateDefaults, - true - ); + $this->assertSame($field->ID(), $schemaStateDefaults['id']); + $this->assertSame('TestTree', $schemaStateDefaults['name']); + $this->assertSame($this->folderIds, $schemaStateDefaults['value']); $items = $field->getItems(); $this->assertCount(2, $items, 'there must be exactly 2 items selected'); @@ -214,38 +191,24 @@ class TreeMultiselectFieldTest extends SapphireTest $field = $this->field->performReadonlyTransformation(); $schemaStateDefaults = $field->getSchemaStateDefaults(); - $this->assertArraySubset( - [ - 'id' => $field->ID(), - 'name' => 'TestTree', - 'value' => 'unchanged' - ], - $schemaStateDefaults, - true - ); + $this->assertSame($field->ID(), $schemaStateDefaults['id']); + $this->assertSame('TestTree', $schemaStateDefaults['name']); + $this->assertSame('unchanged', $schemaStateDefaults['value']); $schemaDataDefaults = $field->getSchemaDataDefaults(); - $this->assertArraySubset( - [ - 'id' => $field->ID(), - 'name' => $this->fieldName, - 'type' => 'text', - 'schemaType' => 'SingleSelect', - 'component' => 'TreeDropdownField', - 'holderId' => sprintf('%s_Holder', $field->ID()), - 'title' => 'Test tree', - 'extraClass' => 'treemultiselectfield_readonly multiple searchable', - 'data' => [ - 'urlTree' => 'field/TestTree/tree', - 'showSearch' => true, - 'emptyString' => '(Search or choose File)', - 'hasEmptyDefault' => false, - 'multiple' => true - ] - ], - $schemaDataDefaults, - true - ); + $this->assertSame($field->ID(), $schemaDataDefaults['id']); + $this->assertSame($this->fieldName, $schemaDataDefaults['name']); + $this->assertSame('text', $schemaDataDefaults['type']); + $this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']); + $this->assertSame('TreeDropdownField', $schemaDataDefaults['component']); + $this->assertSame(sprintf('%s_Holder', $field->ID()), $schemaDataDefaults['holderId']); + $this->assertSame('Test tree', $schemaDataDefaults['title']); + $this->assertSame('treemultiselectfield_readonly multiple searchable', $schemaDataDefaults['extraClass']); + $this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']); + $this->assertSame(true, $schemaDataDefaults['data']['showSearch']); + $this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']); + $this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']); + $this->assertSame(true, $schemaDataDefaults['data']['multiple']); $items = $field->getItems(); $this->assertCount(0, $items, 'there must be 0 selected items'); @@ -264,38 +227,24 @@ class TreeMultiselectFieldTest extends SapphireTest $field = $field->performReadonlyTransformation(); $schemaStateDefaults = $field->getSchemaStateDefaults(); - $this->assertArraySubset( - [ - 'id' => $field->ID(), - 'name' => 'TestTree', - 'value' => $this->folderIds - ], - $schemaStateDefaults, - true - ); + $this->assertSame($field->ID(), $schemaStateDefaults['id']); + $this->assertSame('TestTree', $schemaStateDefaults['name']); + $this->assertSame($this->folderIds, $schemaStateDefaults['value']); $schemaDataDefaults = $field->getSchemaDataDefaults(); - $this->assertArraySubset( - [ - 'id' => $field->ID(), - 'name' => $this->fieldName, - 'type' => 'text', - 'schemaType' => 'SingleSelect', - 'component' => 'TreeDropdownField', - 'holderId' => sprintf('%s_Holder', $field->ID()), - 'title' => 'Test tree', - 'extraClass' => 'treemultiselectfield_readonly multiple searchable', - 'data' => [ - 'urlTree' => 'field/TestTree/tree', - 'showSearch' => true, - 'emptyString' => '(Search or choose File)', - 'hasEmptyDefault' => false, - 'multiple' => true - ] - ], - $schemaDataDefaults, - true - ); + $this->assertSame($field->ID(), $schemaDataDefaults['id']); + $this->assertSame($this->fieldName, $schemaDataDefaults['name']); + $this->assertSame('text', $schemaDataDefaults['type']); + $this->assertSame('SingleSelect', $schemaDataDefaults['schemaType']); + $this->assertSame('TreeDropdownField', $schemaDataDefaults['component']); + $this->assertSame(sprintf('%s_Holder', $field->ID()), $schemaDataDefaults['holderId']); + $this->assertSame('Test tree', $schemaDataDefaults['title']); + $this->assertSame('treemultiselectfield_readonly multiple searchable', $schemaDataDefaults['extraClass']); + $this->assertSame('field/TestTree/tree', $schemaDataDefaults['data']['urlTree']); + $this->assertSame(true, $schemaDataDefaults['data']['showSearch']); + $this->assertSame('(Search or choose File)', $schemaDataDefaults['data']['emptyString']); + $this->assertSame(false, $schemaDataDefaults['data']['hasEmptyDefault']); + $this->assertSame(true, $schemaDataDefaults['data']['multiple']); $items = $field->getItems(); $this->assertCount(2, $items, 'there must be exactly 2 selected items'); From 20134e6a4f77907f560f240a68d1d627a0d23b38 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Sun, 7 Nov 2021 20:26:21 +0000 Subject: [PATCH 05/11] NEW Add FirstPage() and LastPage() to PaginatedList (#10129) --- src/ORM/PaginatedList.php | 20 ++++++++++++++++++-- tests/php/ORM/PaginatedListTest.php | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/ORM/PaginatedList.php b/src/ORM/PaginatedList.php index 200d7b006..92c025194 100644 --- a/src/ORM/PaginatedList.php +++ b/src/ORM/PaginatedList.php @@ -394,12 +394,28 @@ class PaginatedList extends ListDecorator return $this->TotalPages() > 1; } + /** + * @return bool + */ + public function FirstPage() + { + return $this->CurrentPage() == 1; + } + /** * @return bool */ public function NotFirstPage() { - return $this->CurrentPage() != 1; + return !$this->FirstPage(); + } + + /** + * @return bool + */ + public function LastPage() + { + return $this->CurrentPage() == $this->TotalPages(); } /** @@ -407,7 +423,7 @@ class PaginatedList extends ListDecorator */ public function NotLastPage() { - return $this->CurrentPage() < $this->TotalPages(); + return !$this->LastPage(); } /** diff --git a/tests/php/ORM/PaginatedListTest.php b/tests/php/ORM/PaginatedListTest.php index dd9e30b69..29b3c09f4 100644 --- a/tests/php/ORM/PaginatedListTest.php +++ b/tests/php/ORM/PaginatedListTest.php @@ -282,6 +282,14 @@ class PaginatedListTest extends SapphireTest $this->assertFalse($list->MoreThanOnePage()); } + public function testFirstPage() + { + $list = new PaginatedList(new ArrayList()); + $this->assertTrue($list->FirstPage()); + $list->setCurrentPage(2); + $this->assertFalse($list->FirstPage()); + } + public function testNotFirstPage() { $list = new PaginatedList(new ArrayList()); @@ -290,6 +298,16 @@ class PaginatedListTest extends SapphireTest $this->assertTrue($list->NotFirstPage()); } + public function testLastPage() + { + $list = new PaginatedList(new ArrayList()); + $list->setTotalItems(50); + + $this->assertFalse($list->LastPage()); + $list->setCurrentPage(5); + $this->assertTrue($list->LastPage()); + } + public function testNotLastPage() { $list = new PaginatedList(new ArrayList()); From 4b8bc55c4052e2fe4b22ccfe14d76adfda41cf81 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Mon, 8 Nov 2021 09:42:21 +1300 Subject: [PATCH 06/11] DOC Create skeleton for 4.10.0 changelog --- docs/en/04_Changelogs/4.10.0.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/en/04_Changelogs/4.10.0.md diff --git a/docs/en/04_Changelogs/4.10.0.md b/docs/en/04_Changelogs/4.10.0.md new file mode 100644 index 000000000..4c3548b0d --- /dev/null +++ b/docs/en/04_Changelogs/4.10.0.md @@ -0,0 +1,20 @@ +## Overview + +## Features and enhancements {#features-and-enhancements} + +### PHPUnit 9{#phpunit9} + +### Other new features + +## Bugfixes {#bugfixes} + +This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release! + + +## Change Log + + +### Security + + +### Features and Enhancements From 0e6817bb8d2721a531473c502220e3f8c90c7073 Mon Sep 17 00:00:00 2001 From: Bryn Whyman Date: Mon, 8 Nov 2021 15:07:17 +1300 Subject: [PATCH 07/11] DOCS new guidance page on how to upgrade a project (#10132) Co-authored-by: brynwhyman --- .../01_Keeping_projects_up_to_date.md | 52 +++++++++++++++++++ ...ing_project.md => 04_Upgrading_project.md} | 0 docs/en/03_Upgrading/index.md | 4 +- 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md rename docs/en/03_Upgrading/{01_Upgrading_project.md => 04_Upgrading_project.md} (100%) diff --git a/docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md b/docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md new file mode 100644 index 000000000..1b806c9bf --- /dev/null +++ b/docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md @@ -0,0 +1,52 @@ +--- +title: Staying up to date with CMS releases +summary: Guidance on upgrading your website with new recipe releases +--- + +# Upgrading + +Upgrading to new patch versions of the recipe shouldn't take a long time. See [recipes and supported modules](../00_Getting_Started/05_Recipes.md)) documentation to learn more about how recipe versioning is structured. + +## Patch upgrades + +To get the newest patch release of the recipe, just run: + +`composer update` + +This will update the recipe to the new version, and pull in all the new dependencies. A new `composer.lock` file will be generated. Once you are satisfied the site is running as expected, commit both files: + +`git commit composer.* -m "Upgrade the recipe to latest patch release"` + +After you have pushed this commit back to your remote repository you can deploy the change. + +## Minor and major upgrades + +Assuming your project is using one of the [supported recipes](../00_Getting_Started/05_Recipes.md), these will likely take more time as the APIs may change between minor and major releases. For small sites it's possible for minor upgrade to take a day of work, and major upgrades could take several days. Of course this can widely differ depending on each project. + +To upgrade your code, open the root `composer.json` file. Find the lines that reference the recipes, like `silverstripe/recipe-cms` and change the referenced versions to what has been reference in the changelog (as well as any other modules that have a new version). + +For example, assuming that you are currently on version `~4.8.0@stable`, if you wish to upgrade to 4.9.0 you will need to modify your `composer.json` file to explicitly specify the new release branch, here `~4.9.0`: + +```json +"require": { + "silverstripe/recipe-cms": "~4.9.0" +}, +... +``` + +You now need to pull in new dependencies and commit the lock file: + +```bash +composer update +git commit composer.* -m "Upgrade to recipe 4.9.0" +``` + +Push this commit to your remote repository, and continue with your deployment workflow. + +## Cherrypicking the upgrades + +If you like to only upgrade the recipe modules, you can cherry pick what is upgraded using this syntax: + +`composer update silverstripe/recipe-cms` + +This will update only the two specified metapackage modules without touching anything else. You still need to commit resulting `composer.lock`. \ No newline at end of file diff --git a/docs/en/03_Upgrading/01_Upgrading_project.md b/docs/en/03_Upgrading/04_Upgrading_project.md similarity index 100% rename from docs/en/03_Upgrading/01_Upgrading_project.md rename to docs/en/03_Upgrading/04_Upgrading_project.md diff --git a/docs/en/03_Upgrading/index.md b/docs/en/03_Upgrading/index.md index ae09ddf29..391fb02fc 100644 --- a/docs/en/03_Upgrading/index.md +++ b/docs/en/03_Upgrading/index.md @@ -3,6 +3,8 @@ title: Upgrading summary: The following guides will help you upgrade your project or module to Silverstripe CMS 4. --- -The following guides will help you upgrade your project or module to Silverstripe CMS 4. Upgrading a module is very similar to upgrading a Project. The module upgrade guide assumes familiarity with the project upgrade guide. +The following guides will help you upgrade your project. + +There are also key points to help you upgrade your project or module to Silverstripe CMS 4. Upgrading a module is very similar to upgrading a Project. The module upgrade guide assumes familiarity with the project upgrade guide. [CHILDREN] From b3810071564631984c432c7d89d9bf430e28f8ed Mon Sep 17 00:00:00 2001 From: Bryn Whyman Date: Mon, 8 Nov 2021 15:09:51 +1300 Subject: [PATCH 08/11] DOCS more guidance on how to create a module (#10131) The docs on creating a module are pretty light. I've opted to put this in the developer docs and not in the module skeleton repository as I see the later as more of a file skeleton, over a full tutorial space. Co-authored-by: brynwhyman --- .../05_Extending/00_Modules.md | 113 +++++++++++++++--- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/docs/en/02_Developer_Guides/05_Extending/00_Modules.md b/docs/en/02_Developer_Guides/05_Extending/00_Modules.md index 368e2d597..0861a783b 100644 --- a/docs/en/02_Developer_Guides/05_Extending/00_Modules.md +++ b/docs/en/02_Developer_Guides/05_Extending/00_Modules.md @@ -14,20 +14,6 @@ Modules are [Composer packages](https://getcomposer.org/), and are placed in the These packages need to contain either a toplevel `_config` directory or `_config.php` file, as well as a special `type` in their `composer.json` file ([example](https://github.com/silverstripe/silverstripe-module/blob/4/composer.json)). -``` -app/ -| -+-- _config/ -+-- src/ -+-- .. -| -vendor/my_vendor/my_module/ -| -+-- _config/ -+-- composer.json -+-- ... -``` - Like with any Composer package, we recommend declaring your PHP classes through [PSR autoloading](https://getcomposer.org/doc/01-basic-usage.md#autoloading). Silverstripe CMS will automatically discover templates and configuration settings @@ -77,9 +63,104 @@ or share your code with the community. Silverstripe CMS already has certain modules included, for example the `cms` module and core functionality such as commenting and spam protection are also abstracted into modules allowing developers the freedom to choose what they want. +### Create a new directory + The easiest way to get started is our [Module Skeleton](https://github.com/silverstripe/silverstripe-module). -In case you want to share your creation with the community, -read more about [publishing a module](how_tos/publish_a_module). + +First, create a new directory named after your intended module in your main project. It should sit alongside the other modules +such as *silverstripe/framework* and *silverstripe/cms* and use it for the module development: + +`mkdir /vendor/my_vendor/nice_feature` + +Then clone the Module Skeleton to get a headstart with the module files: + +```bash +cd /vendor/my_vendor/nice_feature +git clone git@github.com:silverstripe/silverstripe-module.git . +``` + +### Allow your module to be importable by composer + +You need to set your module up to be importable via composer. For this, edit the new `composer.json` file in the root of +your module. Here is an example for a module that builds on the functionality provided by the `blog` main module (hence the +requirement): + +```json +{ + "name": "my_vendor/nice_feature", + "description": "Short module description", + "type": "silverstripe-vendormodule", + "require": { + "silverstripe/cms": "^4.0", + "silverstripe/framework": "^4.0", + "silverstripe/blog": "^4@dev" + } +} +``` + +After your module is running and tested, you can publish it. Since your module is a self-contained piece of software, it +will constitute a project in itself. The below assumes you are using GitHub and have already created a new GitHub repository for this module. + +Push your module upstream to the empty repository just created: + +```bash + git init + git add -A + git commit -m 'first commit' + git remote add origin git@github.com:my_vendor/nice_feature.git + git push -u origin master +``` + +Once the module is pushed to the repository you should see the code on GitHub. From now on it will be available for +others to clone, as long as they have access (see the note below though: private modules are not deployable). + +### Including a private module in your project + +Including public or private repositories that are not indexed on **Packagist** is different from simply using the `composer require silverstripe/blog` command. We will need to point *composer* to specific URLs. Background information can be found at +[Working with project forks and unreleased +modules](../../getting_started/composer/#working-with-project-forks-and-unreleased-modules). + +For our *nice_module* example module we have just pushed upstream and can add the following lines to your `composer.json` file in the root directory of your main project. + +```json + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:my_vendor/nice_feature.git", + } + ] +``` + +This will add the repository to the list of URLs composer checks when updating the project dependencies. Hence you can +now include the following requirement in the same `composer.json`: + +``` + "require": { + ... + "my_vendor.nice_feature": "*" + } +``` + +Add the module directory name (`nice_feature/`) to `.gitignore` - we will rely on *composer* to update the dependencies so +we don't need to version-control it through the master repository. + +Run `composer update` to pull the module in and update all other dependencies as well. You can also update just this one +module by calling `composer update my_vendor/nice_feature`. + +If you get cryptic composer errors it's worth checking that your module code is fully pushed. This is because composer +can only access the code you have actually pushed to the upstream repository and it may be trying to use the stale +versions of the files. Also, update composer regularly (`composer self-update`). You can also try deleting Composer +cache: `rm -fr ~/.composer/cache`. + +Finally, commit the the modified `composer.json`, `composer.lock` and `.gitignore` files to the repository. The +`composer.lock` serves as a snapshot marker for the dependencies - other developers will be able to `composer install` +exactly the version of the modules you have used in your project, as well as the correct version will be used for the +deployment. Some additional information is available in the [Deploying projects with +composer](https://docs.silverstripe.org/en/4/getting_started/composer/#deploying-projects-with-composer). + +### Open-sourcing your creation for the community to use + +In case you want to share your creation with the community, read more about [publishing a module](how_tos/publish_a_module). ## Module Standard From 5d1cac00e8ec9a9efebff116d7bc5a6eec8f803c Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 9 Nov 2021 11:17:44 +1300 Subject: [PATCH 09/11] MNT Bring back PGSQL test matrix (#10144) --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 529543272..68e14ac06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,12 @@ jobs: - PHPUNIT_TEST=1 - PHPUNIT_SUITE="framework" - COMPOSER_INSTALL_ARG="--prefer-lowest" + - php: 7.4 + env: + - DB=PGSQL + - REQUIRE_INSTALLER="$REQUIRE_RECIPE" + - PHPUNIT_TEST=1 + - PHPUNIT_SUITE="framework" - php: 7.4 env: - DB=MYSQL From a08f43b762343b44e2d1a7b1ac976787c40fd45c Mon Sep 17 00:00:00 2001 From: LiamKearn <76269376+LiamKearn@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:55:06 +1100 Subject: [PATCH 10/11] DOC Fix misleading code docblocks (#10145) --- src/Control/HTTPRequest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Control/HTTPRequest.php b/src/Control/HTTPRequest.php index 6f9212cc2..666f3dda0 100644 --- a/src/Control/HTTPRequest.php +++ b/src/Control/HTTPRequest.php @@ -365,9 +365,9 @@ class HTTPRequest implements ArrayAccess } /** - * Remove an existing HTTP header + * Returns a HTTP Header by name if found in the request * - * @param string $header + * @param string $header Name of the header (Insensitive to case as per ) * @return mixed */ public function getHeader($header) @@ -393,7 +393,7 @@ class HTTPRequest implements ArrayAccess /** * Returns the URL used to generate the page * - * @param bool $includeGetVars whether or not to include the get parameters\ + * @param bool $includeGetVars whether or not to include the get parameters * @return string */ public function getURL($includeGetVars = false) From d1cd9361d82342c7daad5ed3bc6e16a04b5373e6 Mon Sep 17 00:00:00 2001 From: Maxime Rainville Date: Tue, 2 Nov 2021 23:17:24 +1300 Subject: [PATCH 11/11] DOC Update PHP support policy --- .../00_Server_Requirements.md | 12 ++++--- docs/en/04_Changelogs/4.10.0.md | 33 ++++++++++++++----- docs/en/04_Changelogs/4.11.0.md | 31 +++++++++++++++++ 3 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 docs/en/04_Changelogs/4.11.0.md diff --git a/docs/en/00_Getting_Started/00_Server_Requirements.md b/docs/en/00_Getting_Started/00_Server_Requirements.md index 74225d26f..cb6bbc4a4 100644 --- a/docs/en/00_Getting_Started/00_Server_Requirements.md +++ b/docs/en/00_Getting_Started/00_Server_Requirements.md @@ -10,9 +10,9 @@ Silverstripe CMS needs to be installed on a web server. Content authors and webs to access a web-based GUI to do their day-to-day work. Website designers and developers require access to the files on the server to update templates, website logic, and perform upgrades or maintenance. -## PHP +## PHP {php} -* PHP >=7.1 +* PHP >=7.3 * PHP extensions: `ctype`, `dom`, `fileinfo`, `hash`, `intl`, `mbstring`, `session`, `simplexml`, `tokenizer`, `xml` * PHP configuration: `memory_limit` with at least `48M` * PHP extension for image manipulation: Either `gd` or `imagick` @@ -20,6 +20,8 @@ the server to update templates, website logic, and perform upgrades or maintenan Use [phpinfo()](http://php.net/manual/en/function.phpinfo.php) to inspect your configuration. +Silverstripe CMS tracks the official [PHP release support timeline](https://www.php.net/supported-versions.php). When a PHP version reaches end-of-life, Silverstripe CMS drops support for it in the next minor release. + ## Database * MySQL >=5.6 ( @@ -271,11 +273,13 @@ table may be of use: | Silverstripe CMS Version | PHP Version | More information | | -------------------- | ----------- | ---------------- | -| 3.0 - 3.5 | 5.3 - 5.6 | [requirements docs](https://docs.silverstripe.org/en/3.4/getting_started/server_requirements/) +| 3.0 - 3.5 | 5.3 - 5.6 | | | 3.6 | 5.3 - 7.1 | | | 3.7 | 5.3 - 7.4 | [changelog](https://docs.silverstripe.org/en/3/changelogs/3.7.4/) | | 4.0 - 4.4 | 5.6+ | | -| 4.5+ | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) | +| 4.5 - 4.9 | 7.1+ | [blog post](https://www.silverstripe.org/blog/our-plan-for-ending-php-5-6-support-in-silverstripe-4/) | +| 4.10 | 7.3+ | [changelog](/Changelogs/4.10.0#phpeol/) | +| 4.11 + | 7.4+ | [changelog](/Changelogs/4.11.0#phpeol) | ## CMS browser requirements diff --git a/docs/en/04_Changelogs/4.10.0.md b/docs/en/04_Changelogs/4.10.0.md index 4c3548b0d..9d3c4d310 100644 --- a/docs/en/04_Changelogs/4.10.0.md +++ b/docs/en/04_Changelogs/4.10.0.md @@ -1,20 +1,35 @@ +# 4.10.0 (unreleased) + ## Overview +- [Regression test and Security audit](#audit) +- [Dropping support for PHP 7.1 and PHP 7.2](#phpeol) +- [Features and enhancements](#features-and-enhancements) +- [Bugfixes](#bugfixes) + + +## Regression test and Security audit{#audit} + +This release has been comprehensively regression tested and passed to a third party for a security-focused audit. + +While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release. + +## Dropping support for PHP 7.1 and PHP 7.2{#phpeol} + +We've recently updated our [PHP support policy](/Getting_Started/Server_Requirements#php). The immediate affects of this changes are: + +- The Silverstripe CMS Recipe release 4.10.0 drops support for PHP 7.1 and PHP 7.2. Those two PHP releases have been end-of-life for several years now and continued support would detract effort from more valuable work. +- The 4.11 minor release will drop support for PHP 7.3 later this year. +- We expect to drop support for PHP 7 altogether around January 2023. + ## Features and enhancements {#features-and-enhancements} -### PHPUnit 9{#phpunit9} - -### Other new features ## Bugfixes {#bugfixes} This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release! -## Change Log + - -### Security - - -### Features and Enhancements + diff --git a/docs/en/04_Changelogs/4.11.0.md b/docs/en/04_Changelogs/4.11.0.md new file mode 100644 index 000000000..5f32ee96e --- /dev/null +++ b/docs/en/04_Changelogs/4.11.0.md @@ -0,0 +1,31 @@ +# 4.11.0 (unreleased) + +## Overview + +- [Regression test and Security audit](#audit) +- [Dropping support for PHP 7.3](#phpeol) +- [Features and enhancements](#features-and-enhancements) +- [Bugfixes](#bugfixes) + + +## Regression test and Security audit{#audit} + +This release has been comprehensively regression tested and passed to a third party for a security-focused audit. + +While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release. + +## Dropping support for PHP 7.3{#phpeol} + +In accordance with our [PHP support policy](/Getting_Started/Server_Requirements), Silverstripe CMS Recipe release 4.11.0 drops support for PHP 7.3. We expect to drop support for PHP 7 altogether around January 2023. + +## Features and enhancements {#features-and-enhancements} + + +## Bugfixes {#bugfixes} + +This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release! + + + + +