mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge branch 'silverstripe:4' into 4
This commit is contained in:
commit
5e26757a9e
@ -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
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
52
docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md
Normal file
52
docs/en/03_Upgrading/01_Keeping_projects_up_to_date.md
Normal file
@ -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`.
|
@ -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]
|
||||
|
35
docs/en/04_Changelogs/4.10.0.md
Normal file
35
docs/en/04_Changelogs/4.10.0.md
Normal file
@ -0,0 +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}
|
||||
|
||||
|
||||
## 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!
|
||||
|
||||
|
||||
<!--- Changes below this line will be automatically regenerated -->
|
||||
|
||||
<!--- Changes above this line will be automatically regenerated -->
|
31
docs/en/04_Changelogs/4.11.0.md
Normal file
31
docs/en/04_Changelogs/4.11.0.md
Normal file
@ -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!
|
||||
|
||||
|
||||
<!--- Changes below this line will be automatically regenerated -->
|
||||
|
||||
<!--- Changes above this line will be automatically regenerated -->
|
@ -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'
|
||||
|
@ -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 <rfc2616 section 4.2 "Message Headers">)
|
||||
* @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)
|
||||
|
@ -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) {
|
||||
@ -2434,9 +2440,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) {
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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());
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user