mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge branch '3.1'
Conflicts: forms/Form.php forms/FormField.php security/Member.php security/MemberLoginForm.php
This commit is contained in:
commit
d431e98ecf
@ -245,7 +245,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
Requirements::set_backend(new Requirements_Backend());
|
Requirements::set_backend(new Requirements_Backend());
|
||||||
|
|
||||||
// Handle absolute URLs
|
// Handle absolute URLs
|
||||||
if (@parse_url($url, PHP_URL_HOST) != '') {
|
if (parse_url($url, PHP_URL_HOST)) {
|
||||||
$bits = parse_url($url);
|
$bits = parse_url($url);
|
||||||
// If a port is mentioned in the absolute URL, be sure to add that into the
|
// If a port is mentioned in the absolute URL, be sure to add that into the
|
||||||
// HTTP host
|
// HTTP host
|
||||||
|
@ -401,7 +401,12 @@ class Debug {
|
|||||||
$reporter = self::create_debug_view();
|
$reporter = self::create_debug_view();
|
||||||
|
|
||||||
// Coupling alert: This relies on knowledge of how the director gets its URL, it could be improved.
|
// Coupling alert: This relies on knowledge of how the director gets its URL, it could be improved.
|
||||||
$httpRequest = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : @$_REQUEST['url'];
|
$httpRequest = null;
|
||||||
|
if(isset($_SERVER['REQUEST_URI'])) {
|
||||||
|
$httpRequest = $_SERVER['REQUEST_URI'];
|
||||||
|
} elseif(isset($_REQUEST['url'])) {
|
||||||
|
$httpRequest = $_REQUEST['url'];
|
||||||
|
}
|
||||||
if(isset($_SERVER['REQUEST_METHOD'])) $httpRequest = $_SERVER['REQUEST_METHOD'] . ' ' . $httpRequest;
|
if(isset($_SERVER['REQUEST_METHOD'])) $httpRequest = $_SERVER['REQUEST_METHOD'] . ' ' . $httpRequest;
|
||||||
|
|
||||||
$reporter->writeHeader($httpRequest);
|
$reporter->writeHeader($httpRequest);
|
||||||
|
@ -128,8 +128,28 @@ class FixtureBlueprint {
|
|||||||
// Populate all relations
|
// Populate all relations
|
||||||
if($data) foreach($data as $fieldName => $fieldVal) {
|
if($data) foreach($data as $fieldName => $fieldVal) {
|
||||||
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
|
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
|
||||||
|
$obj->write();
|
||||||
|
|
||||||
$parsedItems = array();
|
$parsedItems = array();
|
||||||
|
|
||||||
|
if(is_array($fieldVal)) {
|
||||||
|
// handle lists of many_many relations. Each item can
|
||||||
|
// specify the many_many_extraFields against each
|
||||||
|
// related item.
|
||||||
|
foreach($fieldVal as $relVal) {
|
||||||
|
$item = key($relVal);
|
||||||
|
$id = $this->parseValue($item, $fixtures);
|
||||||
|
$parsedItems[] = $id;
|
||||||
|
|
||||||
|
array_shift($relVal);
|
||||||
|
|
||||||
|
$obj->getManyManyComponents($fieldName)->add(
|
||||||
|
$id, $relVal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
$items = preg_split('/ *, */',trim($fieldVal));
|
$items = preg_split('/ *, */',trim($fieldVal));
|
||||||
|
|
||||||
foreach($items as $item) {
|
foreach($items as $item) {
|
||||||
// Check for correct format: =><relationname>.<identifier>.
|
// Check for correct format: =><relationname>.<identifier>.
|
||||||
// Ignore if the item has already been replaced with a numeric DB identifier
|
// Ignore if the item has already been replaced with a numeric DB identifier
|
||||||
@ -144,12 +164,13 @@ class FixtureBlueprint {
|
|||||||
|
|
||||||
$parsedItems[] = $this->parseValue($item, $fixtures);
|
$parsedItems[] = $this->parseValue($item, $fixtures);
|
||||||
}
|
}
|
||||||
$obj->write();
|
|
||||||
if($obj->has_many($fieldName)) {
|
if($obj->has_many($fieldName)) {
|
||||||
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
$obj->getComponents($fieldName)->setByIDList($parsedItems);
|
||||||
} elseif($obj->many_many($fieldName)) {
|
} elseif($obj->many_many($fieldName)) {
|
||||||
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} elseif($obj->has_one($fieldName)) {
|
} elseif($obj->has_one($fieldName)) {
|
||||||
// Sets has_one with relation name
|
// Sets has_one with relation name
|
||||||
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);
|
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);
|
||||||
|
@ -167,8 +167,8 @@ class SS_Log {
|
|||||||
$message = array(
|
$message = array(
|
||||||
'errno' => '',
|
'errno' => '',
|
||||||
'errstr' => $message,
|
'errstr' => $message,
|
||||||
'errfile' => @$lastTrace['file'],
|
'errfile' => isset($lastTrace['file']) ? $lastTrace['file'] : null,
|
||||||
'errline' => @$lastTrace['line'],
|
'errline' => isset($lastTrace['line']) ? $lastTrace['line'] : null,
|
||||||
'errcontext' => $trace
|
'errcontext' => $trace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
|
|||||||
$errorType = 'Notice';
|
$errorType = 'Notice';
|
||||||
$colour = 'grey';
|
$colour = 'grey';
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
$errorType = $event['priorityName'];
|
||||||
|
$colour = 'grey';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!is_array($event['message'])) {
|
if(!is_array($event['message'])) {
|
||||||
@ -63,8 +66,8 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
|
|||||||
$relfile = Director::makeRelative($errfile);
|
$relfile = Director::makeRelative($errfile);
|
||||||
if($relfile && $relfile[0] == '/') $relfile = substr($relfile, 1);
|
if($relfile && $relfile[0] == '/') $relfile = substr($relfile, 1);
|
||||||
|
|
||||||
$host = @$_SERVER['HTTP_HOST'];
|
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
|
||||||
$uri = @$_SERVER['REQUEST_URI'];
|
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
|
||||||
|
|
||||||
$subject = "[$errorType] in $relfile:{$errline} (http://{$host}{$uri})";
|
$subject = "[$errorType] in $relfile:{$errline} (http://{$host}{$uri})";
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ class SS_LogErrorFileFormatter implements Zend_Log_Formatter_Interface {
|
|||||||
case 'NOTICE':
|
case 'NOTICE':
|
||||||
$errtype = 'Notice';
|
$errtype = 'Notice';
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
$errtype = $event['priorityName'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$urlSuffix = '';
|
$urlSuffix = '';
|
||||||
|
@ -408,8 +408,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
|||||||
/**
|
/**
|
||||||
* Get an object from the fixture.
|
* Get an object from the fixture.
|
||||||
*
|
*
|
||||||
* @param $className The data class, as specified in your fixture file. Parent classes won't work
|
* @param string $className The data class, as specified in your fixture file. Parent classes won't work
|
||||||
* @param $identifier The identifier string, as provided in your fixture file
|
* @param string $identifier The identifier string, as provided in your fixture file
|
||||||
|
*
|
||||||
|
* @return DataObject
|
||||||
*/
|
*/
|
||||||
protected function objFromFixture($className, $identifier) {
|
protected function objFromFixture($className, $identifier) {
|
||||||
$obj = $this->getFixtureFactory()->get($className, $identifier);
|
$obj = $this->getFixtureFactory()->get($className, $identifier);
|
||||||
|
@ -395,11 +395,15 @@ class InstallRequirements {
|
|||||||
*/
|
*/
|
||||||
function findWebserver() {
|
function findWebserver() {
|
||||||
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
|
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
|
||||||
$webserver = @$_SERVER['SERVER_SIGNATURE'];
|
if(!empty($_SERVER['SERVER_SIGNATURE'])) {
|
||||||
if(!$webserver) $webserver = @$_SERVER['SERVER_SOFTWARE'];
|
$webserver = $_SERVER['SERVER_SIGNATURE'];
|
||||||
|
} elseif(!empty($_SERVER['SERVER_SOFTWARE'])) {
|
||||||
|
$webserver = $_SERVER['SERVER_SOFTWARE'];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if($webserver) return strip_tags(trim($webserver));
|
return strip_tags(trim($webserver));
|
||||||
else return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1125,7 +1129,7 @@ class InstallRequirements {
|
|||||||
$this->testing($testDetails);
|
$this->testing($testDetails);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if(!@$result['cannotCreate']) {
|
if(empty($result['cannotCreate'])) {
|
||||||
$testDetails[2] .= ". Please create the database manually.";
|
$testDetails[2] .= ". Please create the database manually.";
|
||||||
} else {
|
} else {
|
||||||
$testDetails[2] .= " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
|
$testDetails[2] .= " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
|
||||||
@ -1217,7 +1221,7 @@ class InstallRequirements {
|
|||||||
$section = $testDetails[0];
|
$section = $testDetails[0];
|
||||||
$test = $testDetails[1];
|
$test = $testDetails[1];
|
||||||
|
|
||||||
$this->tests[$section][$test] = array("error", @$testDetails[2]);
|
$this->tests[$section][$test] = array("error", isset($testDetails[2]) ? $testDetails[2] : null);
|
||||||
$this->errors[] = $testDetails;
|
$this->errors[] = $testDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1225,7 +1229,7 @@ class InstallRequirements {
|
|||||||
$section = $testDetails[0];
|
$section = $testDetails[0];
|
||||||
$test = $testDetails[1];
|
$test = $testDetails[1];
|
||||||
|
|
||||||
$this->tests[$section][$test] = array("warning", @$testDetails[2]);
|
$this->tests[$section][$test] = array("warning", isset($testDetails[2]) ? $testDetails[2] : null);
|
||||||
$this->warnings[] = $testDetails;
|
$this->warnings[] = $testDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,34 +2,11 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
### Default current Versioned "stage" to "Live" rather than "Stage"
|
* Security: Require ADMIN for ?flush=1&isDev=1 ([SS-2014-001](http://www.silverstripe.org/ss-2014-001-require-admin-for-flush1-and-isdev1))
|
||||||
|
* Security: XSS in third party library (SWFUpload) ([SS-2014-002](http://www.silverstripe.org/ss-2014-002-xss-in-third-party-library-swfupload/))
|
||||||
|
|
||||||
Previously only the controllers responsible for page and CMS display
|
## Changelog
|
||||||
(`LeftAndMain` and `ContentController`) explicitly set a stage through
|
|
||||||
`Versioned::choose_site_stage()`. Unless this method is called,
|
|
||||||
the default stage will be "Stage", showing draft content.
|
|
||||||
Any direct subclasses of `Controller` interacting with "versioned" objects
|
|
||||||
are vulnerable to exposing unpublished content, unless `choose_site_stage()`
|
|
||||||
is called explicitly in their own logic.
|
|
||||||
|
|
||||||
In order to provide more secure default behaviour, we have changed
|
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)
|
||||||
`choose_site_stage()` to be called on all requests, defaulting to the "Live" stage.
|
* [cms](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)
|
||||||
If your logic relies on querying draft content, use `Versioned::reading_stage('Stage')`.
|
* [installer](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)
|
||||||
|
|
||||||
Important: The `choose_site_stage()` call only deals with setting the default stage,
|
|
||||||
and doesn't check if the user is authenticated to view it. As with any other controller logic,
|
|
||||||
please use `DataObject->canView()` to determine permissions.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyController extends Controller {
|
|
||||||
private static $allowed_actions = array('showpage');
|
|
||||||
public function showpage($request) {
|
|
||||||
$page = Page::get()->byID($request->param('ID'));
|
|
||||||
if(!$page->canView()) return $this->httpError(401);
|
|
||||||
// continue with authenticated logic...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
### API Changes
|
|
||||||
|
|
||||||
* 2013-08-03 [0e7231f](https://github.com/silverstripe/sapphire/commit/0e7231f) Disable discontinued Google Spellcheck in TinyMCE (Ingo Schommer)
|
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
* Security: Require ADMIN for ?flush=1&isDev=1 ([SS-2014-001](http://www.silverstripe.org/ss-2014-001-require-admin-for-flush1-and-isdev1))
|
||||||
|
* Security: XSS in third party library (SWFUpload) ([SS-2014-002](http://www.silverstripe.org/ss-2014-002-xss-in-third-party-library-swfupload/))
|
||||||
|
* Security: SiteTree.ExtraMeta allows JavaScript for malicious CMS authors ([SS-2014-003](http://www.silverstripe.org/ss-2014-003-extrameta-allows-javascript-for-malicious-cms-authors-/))
|
||||||
* Better loading performance when using multiple `UploadField` instances
|
* Better loading performance when using multiple `UploadField` instances
|
||||||
* Option for `force_js_to_bottom` on `Requirements` class (ignoring inline `<script>` tags)
|
* Option for `force_js_to_bottom` on `Requirements` class (ignoring inline `<script>` tags)
|
||||||
* Added `ListDecorator->filterByCallback()` for more sophisticated filtering
|
* Added `ListDecorator->filterByCallback()` for more sophisticated filtering
|
||||||
@ -13,4 +16,14 @@
|
|||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
|
### SiteTree.ExtraMeta allows JavaScript for malicious CMS authors
|
||||||
|
|
||||||
|
If you have previously used the `SiteTree.ExtraMeta` field for `<head>` markup
|
||||||
|
other than its intended use case (`<meta>` and `<link>`), please consult
|
||||||
|
[SS-2014-003](http://www.silverstripe.org/ss-2014-003-extrameta-allows-javascript-for-malicious-cms-authors-/).
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3)
|
||||||
|
* [cms](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3)
|
||||||
|
* [installer](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3)
|
12
docs/en/changelogs/rc/3.0.9-rc1.md
Normal file
12
docs/en/changelogs/rc/3.0.9-rc1.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 3.0.9-rc1 (2014-02-19)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
* Security: Require ADMIN for ?flush=1&isDev=1 ([SS-2014-001](http://www.silverstripe.org/ss-2014-001-require-admin-for-flush1-and-isdev1))
|
||||||
|
* Security: XSS in third party library (SWFUpload) ([SS-2014-002](http://www.silverstripe.org/ss-2014-002-xss-in-third-party-library-swfupload/))
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9-rc1)
|
||||||
|
* [cms](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9-rc1)
|
||||||
|
* [installer](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9-rc1)
|
@ -2,12 +2,28 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
* ExtraMeta fields can now only contain `meta` and `link` elements
|
* Security: Require ADMIN for ?flush=1&isDev=1 ([SS-2014-001](http://www.silverstripe.org/ss-2014-001-require-admin-for-flush1-and-isdev1))
|
||||||
|
* Security: XSS in third party library (SWFUpload) ([SS-2014-002](http://www.silverstripe.org/ss-2014-002-xss-in-third-party-library-swfupload/))
|
||||||
|
* Security: SiteTree.ExtraMeta allows JavaScript for malicious CMS authors ([SS-2014-003](http://www.silverstripe.org/ss-2014-003-extrameta-allows-javascript-for-malicious-cms-authors-/))
|
||||||
|
* Better loading performance when using multiple `UploadField` instances
|
||||||
|
* Option for `force_js_to_bottom` on `Requirements` class (ignoring inline `<script>` tags)
|
||||||
|
* Added `ListDecorator->filterByCallback()` for more sophisticated filtering
|
||||||
|
* New `DataList` filters: `LessThanOrEqualFilter` and `GreaterThanOrEqualFilter`
|
||||||
|
* "Cancel" button on "Add Page" form
|
||||||
|
* Better code hinting on magic properties (for IDE autocompletion)
|
||||||
|
* Increased Behat test coverage (editing HTML content, managing page permissions)
|
||||||
|
* Support for PHPUnit 3.8
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
### ExtraMeta fields can now only contain `meta` and `link` elements
|
### SiteTree.ExtraMeta allows JavaScript for malicious CMS authors
|
||||||
|
|
||||||
Previously ExtraMeta fields could contain any HTML elements. From 3.1.3-rc1 the contents are filtered
|
If you have previously used the `SiteTree.ExtraMeta` field for `<head>` markup
|
||||||
on write to only allow `meta` and `link` elements. The first time after upgrading that you save a page
|
other than its intended use case (`<meta>` and `<link>`), please consult
|
||||||
that has other elements in ExtraMeta they will be deleted.
|
[SS-2014-003](http://www.silverstripe.org/ss-2014-003-extrameta-allows-javascript-for-malicious-cms-authors-/).
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3-rc1)
|
||||||
|
* [cms](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3-rc1)
|
||||||
|
* [installer](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.1.3-rc1)
|
12
docs/en/changelogs/rc/3.1.3-rc2.md
Normal file
12
docs/en/changelogs/rc/3.1.3-rc2.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 3.1.3-rc2
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
* Fixed regression around CMS loading in IE8
|
||||||
|
* Fixed regression in folder creation on upload
|
||||||
|
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
* 2014-02-20 [ebeb663](https://github.com/silverstripe/sapphire/commit/ebeb663) Fixed critical issue with Folder::find_or_make failing to handle invalid filename characters BUG Fix UploadField duplicate checking with invalid folderName (Damian Mooyman)
|
||||||
|
* 2014-02-19 [a681bd7](https://github.com/silverstripe/sapphire/commit/a681bd7) IE8 support in jquery.ondemand.js (fixes #2872) (Loz Calver)
|
@ -71,7 +71,7 @@ Example `mysite/_config.php`:
|
|||||||
// Customized configuration for running with different database settings.
|
// Customized configuration for running with different database settings.
|
||||||
// Ensure this code comes after ConfigureFromEnv.php
|
// Ensure this code comes after ConfigureFromEnv.php
|
||||||
if(Director::isDev()) {
|
if(Director::isDev()) {
|
||||||
if($db = @$_GET['db']) {
|
if(isset($_GET['db']) && ($db = $_GET['db'])) {
|
||||||
global $databaseConfig;
|
global $databaseConfig;
|
||||||
if($db == 'sqlite3') $databaseConfig['type'] = 'SQLite3Database';
|
if($db == 'sqlite3') $databaseConfig['type'] = 'SQLite3Database';
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ Composer updates regularly, so you should run this command fairly often. These i
|
|||||||
|
|
||||||
## Create a new site
|
## Create a new site
|
||||||
|
|
||||||
Composer can create a new site for you, using the installer as a template:
|
Composer can create a new site for you, using the installer as a template (by default composer will download the latest stable version):
|
||||||
|
|
||||||
composer create-project silverstripe/installer ./my/website/folder
|
composer create-project silverstripe/installer ./my/website/folder
|
||||||
|
|
||||||
@ -50,8 +50,7 @@ For example, on OS X, you might use a subdirectory of `~/Sites`.
|
|||||||
As long as your web server is up and running, this will get all the code that you need.
|
As long as your web server is up and running, this will get all the code that you need.
|
||||||
Now visit the site in your web browser, and the installation process will be completed.
|
Now visit the site in your web browser, and the installation process will be completed.
|
||||||
|
|
||||||
By default composer will download the latest stable version. You can also specify
|
You can also specify a version to download that version explicitly, i.e. this will download the older `3.0.3` release:
|
||||||
a version to download that version explicitly, i.e. this will download the older `3.0.3` release:
|
|
||||||
|
|
||||||
composer create-project silverstripe/installer ./my/website/folder 3.0.3
|
composer create-project silverstripe/installer ./my/website/folder 3.0.3
|
||||||
|
|
||||||
@ -116,7 +115,7 @@ So you want to contribute to SilverStripe? Fantastic! You can do this with compo
|
|||||||
You have to tell composer three things in order to be able to do this:
|
You have to tell composer three things in order to be able to do this:
|
||||||
|
|
||||||
- Keep the full git repository information
|
- Keep the full git repository information
|
||||||
- Include dependancies marked as "developer" requirements
|
- Include dependencies marked as "developer" requirements
|
||||||
- Use the development version, not the latest stable version
|
- Use the development version, not the latest stable version
|
||||||
|
|
||||||
The first two steps are done as part of the initial create project using additional arguments.
|
The first two steps are done as part of the initial create project using additional arguments.
|
||||||
@ -233,7 +232,7 @@ For more information, read the ["Repositories" chapter of the Composer documenta
|
|||||||
|
|
||||||
### Forks and branch names
|
### Forks and branch names
|
||||||
|
|
||||||
Generally, you should keep using the same pattern of branch names as the main repositories does. If your version is a fork of 3.0, then call the branch `3.0`, not `3.0-myproj` or `myproj`. Otherwise, the depenency resolution gets confused.
|
Generally, you should keep using the same pattern of branch names as the main repositories does. If your version is a fork of 3.0, then call the branch `3.0`, not `3.0-myproj` or `myproj`. Otherwise, the dependency resolution gets confused.
|
||||||
|
|
||||||
Sometimes, however, this isn't feasible. For example, you might have a number of project forks stored in a single repository, such as your personal github fork of a project. Or you might be testing/developing a feature branch. Or it might just be confusing to other team members to call the branch of your modified version `3.0`.
|
Sometimes, however, this isn't feasible. For example, you might have a number of project forks stored in a single repository, such as your personal github fork of a project. Or you might be testing/developing a feature branch. Or it might just be confusing to other team members to call the branch of your modified version `3.0`.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Nginx
|
# Nginx
|
||||||
|
|
||||||
These instructions are also covered in less detail on the
|
These instructions are also covered on the
|
||||||
[Nginx Wiki](http://wiki.nginx.org/SilverStripe).
|
[Nginx Wiki](http://wiki.nginx.org/SilverStripe).
|
||||||
|
|
||||||
The prerequisite is that you have already installed Nginx and you are
|
The prerequisite is that you have already installed Nginx and you are
|
||||||
@ -18,150 +18,79 @@ But enough of the disclaimer, on to the actual configuration — typically in `n
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name example.com;
|
root /path/to/ss/folder;
|
||||||
|
|
||||||
root /var/www/example.com;
|
server_name site.com www.site.com;
|
||||||
|
|
||||||
# SSL configuration (optional, but recommended for security)
|
|
||||||
# (remember to actually force logins to use ssl)
|
|
||||||
include ssl
|
|
||||||
|
|
||||||
include silverstripe3.conf;
|
|
||||||
include htaccess.conf;
|
|
||||||
|
|
||||||
# rest of the server section is optional, but helpful
|
|
||||||
# maintenance page if it exists
|
|
||||||
error_page 503 @maintenance;
|
|
||||||
if (-f $document_root/maintenance.html ) {
|
|
||||||
return 503;
|
|
||||||
}
|
|
||||||
location @maintenance {
|
|
||||||
try_files /maintenance.html =503;
|
|
||||||
}
|
|
||||||
|
|
||||||
# always show SilverStripe's version of 500 error page
|
|
||||||
error_page 500 /assets/error-500.html;
|
|
||||||
|
|
||||||
# let the user's browser cache static files (e.g. 2 weeks)
|
|
||||||
expires 2w;
|
|
||||||
|
|
||||||
# in case your machine is slow, increase the timeout
|
|
||||||
# (also remembers php's own timeout settings)
|
|
||||||
#fastcgi_read_timeout 300s;
|
|
||||||
}
|
|
||||||
|
|
||||||
Here is the include file `silverstripe3.conf`:
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri @silverstripe;
|
try_files $uri /framework/main.php?url=$uri&$query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
# only needed for installation - disable this location (and remove the
|
error_page 404 /assets/error-404.html;
|
||||||
# index.php and install.php files) after you installed SilverStripe
|
error_page 500 /assets/error-500.html;
|
||||||
# (you did read the blogentry linked above, didn't you)
|
|
||||||
location ~ ^/(index|install).php {
|
|
||||||
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
|
|
||||||
include fastcgi.conf;
|
|
||||||
fastcgi_pass unix:/run/php-fpm/php-fpm-silverstripe.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
# whitelist php files that are called directly and need to be interpreted
|
|
||||||
location = /framework/thirdparty/tinymce/tiny_mce_gzip.php {
|
|
||||||
include fastcgi.conf;
|
|
||||||
fastcgi_pass unix:/run/php-fpm/php-fpm-silverstripe.sock;
|
|
||||||
}
|
|
||||||
location = /framework/thirdparty/tinymce-spellchecker/rpc.php {
|
|
||||||
include fastcgi.conf;
|
|
||||||
fastcgi_pass unix:/run/php-fpm/php-fpm-silverstripe.sock;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @silverstripe {
|
|
||||||
expires off;
|
|
||||||
include fastcgi.conf;
|
|
||||||
fastcgi_pass unix:/run/php-fpm/php-fpm-silverstripe.sock;
|
|
||||||
# note that specifying a fixed script already protects against execution
|
|
||||||
# of arbitrary files, but remember the advice above for any other rules
|
|
||||||
# you add yourself (monitoring, etc,....)
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root/framework/main.php;
|
|
||||||
fastcgi_param SCRIPT_NAME /framework/main.php;
|
|
||||||
fastcgi_param QUERY_STRING url=$uri&$args;
|
|
||||||
|
|
||||||
# tuning is up to your expertise, but buffer_size needs to be >= 8k,
|
|
||||||
# otherwise you'll get "upstream sent too big header while reading
|
|
||||||
# response header from upstream" errors.
|
|
||||||
fastcgi_buffer_size 8k;
|
|
||||||
#fastcgi_buffers 4 32k;
|
|
||||||
#fastcgi_busy_buffers_size 64k;
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
|
||||||
With only the above configuration, nginx would hand out any existing file
|
|
||||||
uninterpreted, so it would happily serve your precious configuration files,
|
|
||||||
including all your private api-keys and whatnot to any random visitor. So you
|
|
||||||
**must** restrict access further.
|
|
||||||
</div>
|
|
||||||
You don't need to use separate files, but it is easier to have the permissive
|
|
||||||
rules distinct from the restricting ones.
|
|
||||||
|
|
||||||
Here is the include file `htaccess.conf`:
|
|
||||||
|
|
||||||
# Don't try to find nonexisting stuff in assets (esp. don't pass through php)
|
|
||||||
location ^~ /assets/ {
|
location ^~ /assets/ {
|
||||||
|
sendfile on;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny access to silverstripe-cache, vendor or composer.json/.lock
|
location ~ /framework/.*(main|rpc|tiny_mce_gzip)\.php$ {
|
||||||
location ^~ /silverstripe-cache/ {
|
fastcgi_keep_conn on;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /(mysite|framework|cms)/.*\.(php|php3|php4|php5|phtml|inc)$ {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ /\.. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.ss$ {
|
||||||
|
satisfy any;
|
||||||
|
allow 127.0.0.1;
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ web\.config$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.ya?ml$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ /vendor/ {
|
location ^~ /vendor/ {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
location ~ /composer\.(json|lock) {
|
|
||||||
|
location ~* /silverstripe-cache/ {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Don't serve up any "hidden" files or directories
|
location ~* composer\.(json|lock)$ {
|
||||||
# (starting with dot, like .htaccess or .git)
|
|
||||||
# also don't serve web.config files
|
|
||||||
location ~ /(\.|web\.config) {
|
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Block access to yaml files (and don't forget about backup
|
location ~* /(cms|framework)/silverstripe_version$ {
|
||||||
# files that editors tend to leave behind)
|
|
||||||
location ~ \.(yml|bak|swp)$ {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
location ~ ~$ {
|
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
# generally don't serve any php-like files
|
location ~ \.php$ {
|
||||||
# (as they exist, they would be served as regular files, and not interpreted.
|
fastcgi_keep_conn on;
|
||||||
# But as those can contain configuration data, this is bad nevertheless)
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
# If needed, you can always whitelist entries.
|
fastcgi_index index.php;
|
||||||
location ~ \.(php|php[345]|phtml|inc)$ {
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
deny all;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
location ~ ^/(cms|framework)/silverstripe_version$ {
|
|
||||||
deny all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Here is the optional include file `ssl`:
|
The above configuration sets up a virtual host `site.com` with
|
||||||
|
rewrite rules suited for SilverStripe. The location block for php files
|
||||||
listen 443 ssl;
|
passes all php scripts to the FastCGI-wrapper via a TCP socket.
|
||||||
ssl_certificate server.crt;
|
|
||||||
ssl_certificate_key server.key;
|
|
||||||
ssl_session_timeout 5m;
|
|
||||||
ssl_protocols SSLv3 TLSv1;
|
|
||||||
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
|
|
||||||
|
|
||||||
The above configuration sets up a virtual host `example.com` with
|
|
||||||
rewrite rules suited for SilverStripe. The location block named
|
|
||||||
`@silverstripe` passes all requests that aren't matched by one of the other
|
|
||||||
location rules (and cannot be satisfied by serving an existing file) to
|
|
||||||
SilverStripe framework's main.php script, that is run by the FastCGI-wrapper,
|
|
||||||
that in turn is accessed via a Unix socket.
|
|
||||||
|
|
||||||
Now you can proceed with the SilverStripe installation normally.
|
Now you can proceed with the SilverStripe installation normally.
|
||||||
|
@ -208,7 +208,7 @@ Non-textual elements (such as images and their manipulations) can also be used i
|
|||||||
'HeroImage' => 'Image'
|
'HeroImage' => 'Image'
|
||||||
);
|
);
|
||||||
private static $summary_fields = array(
|
private static $summary_fields = array(
|
||||||
'Name' => 'Name,
|
'Name' => 'Name',
|
||||||
'HeroImage.CMSThumbnail' => 'Hero Image'
|
'HeroImage.CMSThumbnail' => 'Hero Image'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,6 @@ Note how the configuration happens in different entwine namespaces
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}(jQuery));
|
}(jQuery));
|
||||||
|
|
||||||
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'
|
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'
|
||||||
|
@ -97,9 +97,10 @@ plus some `width` and `height` arguments. We'll add defaults to those in our sho
|
|||||||
|
|
||||||
The hard bits are taken care of (parsing out the shortcodes), everything we need to do is a bit of string replacement.
|
The hard bits are taken care of (parsing out the shortcodes), everything we need to do is a bit of string replacement.
|
||||||
CMS users still need to remember the specific syntax, but these shortcodes can form the basis
|
CMS users still need to remember the specific syntax, but these shortcodes can form the basis
|
||||||
for more advanced editing interfaces (with visual placeholders). See the built-in `[embed]` shortcode as an example
|
for more advanced editing interfaces (with visual placeholders). See the built-in `embed` shortcode as an example
|
||||||
for coupling shortcodes with a form to create and edit placeholders.
|
for coupling shortcodes with a form to create and edit placeholders.
|
||||||
|
|
||||||
|
|
||||||
## Built-in Shortcodes
|
## Built-in Shortcodes
|
||||||
|
|
||||||
SilverStripe comes with several shortcode parsers already.
|
SilverStripe comes with several shortcode parsers already.
|
||||||
|
@ -21,7 +21,7 @@ In your controller (e.g. `mysite/code/Page.php`):
|
|||||||
// either specify the css file manually
|
// either specify the css file manually
|
||||||
Requirements::css("mymodule/css/my.css", "screen,projection");
|
Requirements::css("mymodule/css/my.css", "screen,projection");
|
||||||
// or mention the css filename and SilverStripe will get the file from the current theme and add it to the template
|
// or mention the css filename and SilverStripe will get the file from the current theme and add it to the template
|
||||||
Requirements::themedCSS('print', 'print');
|
Requirements::themedCSS('print', null,'print');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -720,7 +720,7 @@ an object, not for displaying the objects contained in the relation.
|
|||||||
## Validation and Constraints
|
## Validation and Constraints
|
||||||
|
|
||||||
Traditionally, validation in SilverStripe has been mostly handled on the
|
Traditionally, validation in SilverStripe has been mostly handled on the
|
||||||
controller through [form validation](/topics/form-validation).
|
controller through [form validation](/topics/forms#form-validation).
|
||||||
|
|
||||||
While this is a useful approach, it can lead to data inconsistencies if the
|
While this is a useful approach, it can lead to data inconsistencies if the
|
||||||
record is modified outside of the controller and form context.
|
record is modified outside of the controller and form context.
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
# Form Validation
|
|
||||||
|
|
||||||
SilverStripe provides PHP form validation out of the box,
|
|
||||||
but doesn't come with any built-in JavaScript validation
|
|
||||||
(the previously used `Validator.js` approach has been deprecated).
|
|
||||||
|
|
||||||
## Required Fields
|
|
||||||
|
|
||||||
Validators are implemented as an argument to the `[api:Form]` constructor,
|
|
||||||
and are subclasses of the abstract `[api:Validator]` base class.
|
|
||||||
The only implementation which comes with SilverStripe is
|
|
||||||
the `[api:RequiredFields]` class, which ensures fields are filled out
|
|
||||||
when the form is submitted.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
public function Form() {
|
|
||||||
$form = new Form($this, 'Form',
|
|
||||||
new FieldList(
|
|
||||||
new TextField('MyRequiredField'),
|
|
||||||
new TextField('MyOptionalField')
|
|
||||||
),
|
|
||||||
new FieldList(
|
|
||||||
new FormAction('submit', 'Submit form')
|
|
||||||
),
|
|
||||||
new RequiredFields(array('MyRequiredField'))
|
|
||||||
);
|
|
||||||
// Optional: Add a CSS class for custom styling
|
|
||||||
$form->dataFieldByName('MyRequiredField')->addExtraClass('required');
|
|
||||||
return $form;
|
|
||||||
}
|
|
||||||
|
|
||||||
## Form Field Validation
|
|
||||||
|
|
||||||
Form fields are responsible for validating the data they process,
|
|
||||||
through the `[api:FormField->validate()] method. There are many fields
|
|
||||||
for different purposes (see ["form field types"](/reference/form-field-types) for a full list).
|
|
||||||
|
|
||||||
## Adding your own validation messages
|
|
||||||
|
|
||||||
In many cases, you want to add PHP validation which is more complex than
|
|
||||||
validating the format or existence of a single form field input.
|
|
||||||
For example, you might want to have dependent validation on
|
|
||||||
a postcode which depends on the country you've selected in a different field.
|
|
||||||
|
|
||||||
There's two ways to go about this: Either you can attach a custom error message
|
|
||||||
to a specific field, or a generic message for the whole form.
|
|
||||||
|
|
||||||
Example: Validate postcodes based on the selected country (on the controller).
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class MyController extends Controller {
|
|
||||||
private static $allowed_actions = array('Form');
|
|
||||||
public function Form() {
|
|
||||||
return Form::create($this, 'Form',
|
|
||||||
new FieldList(
|
|
||||||
new NumericField('Postcode'),
|
|
||||||
new CountryDropdownField('Country')
|
|
||||||
),
|
|
||||||
new FieldList(
|
|
||||||
new FormAction('submit', 'Submit form')
|
|
||||||
),
|
|
||||||
new RequiredFields(array('Country'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
public function submit($data, $form) {
|
|
||||||
// At this point, RequiredFields->validate() will have been called already,
|
|
||||||
// so we can assume that the values exist.
|
|
||||||
|
|
||||||
// German postcodes need to be five digits
|
|
||||||
if($data['Country'] == 'de' && isset($data['Postcode']) && strlen($data['Postcode']) != 5) {
|
|
||||||
$form->addErrorMessage('Postcode', 'Need five digits for German postcodes', 'bad');
|
|
||||||
return $this->redirectBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global validation error (not specific to form field)
|
|
||||||
if($data['Country'] == 'IR' && isset($data['Postcode']) && $data['Postcode']) {
|
|
||||||
$form->sessionMessage("Ireland doesn't have postcodes!", 'bad');
|
|
||||||
return $this->redirectBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
// continue normal processing...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
## JavaScript Validation
|
|
||||||
|
|
||||||
While there are no built-in JavaScript validation handlers in SilverStripe,
|
|
||||||
the `FormField` API is flexible enough to provide the information required
|
|
||||||
in order to plug in custom libraries.
|
|
||||||
|
|
||||||
### HTML5 attributes
|
|
||||||
|
|
||||||
HTML5 specifies some built-in form validations ([source](http://www.w3.org/wiki/HTML5_form_additions)),
|
|
||||||
which are evaluated by modern browsers without any need for JavaScript.
|
|
||||||
SilverStripe supports this by allowing to set custom attributes on fields.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Markup contains <input type="text" required />
|
|
||||||
TextField::create('MyText')->setAttribute('required', true);
|
|
||||||
|
|
||||||
// Markup contains <input type="url" pattern="https?://.+" />
|
|
||||||
TextField::create('MyText')
|
|
||||||
->setAttribute('type', 'url')
|
|
||||||
->setAttribute('pattern', 'https?://.+')
|
|
||||||
|
|
||||||
### HTML5 metadata
|
|
||||||
|
|
||||||
In addition, HTML5 elements can contain custom data attributes with the `data-` prefix.
|
|
||||||
These are general purpose attributes, but can be used to hook in your own validation.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
// Validate a specific date format (in PHP)
|
|
||||||
// Markup contains <input type="text" data-dateformat="dd.MM.yyyy" />
|
|
||||||
DateField::create('MyDate')->setConfig('dateformat', 'dd.MM.yyyy');
|
|
||||||
|
|
||||||
// Limit extensions on upload (in PHP)
|
|
||||||
// Markup contains <input type="file" data-allowed-extensions="jpg,jpeg,gif" />
|
|
||||||
$exts = array('jpg', 'jpeg', 'gif');
|
|
||||||
$validator = new Upload_Validator();
|
|
||||||
$validator->setAllowedExtensions($exts);
|
|
||||||
$upload = Upload::create()->setValidator($validator);
|
|
||||||
$fileField = FileField::create('MyFile')->setUpload(new);
|
|
||||||
$fileField->setAttribute('data-allowed-extensions', implode(',', $exts));
|
|
||||||
|
|
||||||
Note that these examples don't have any effect on the client as such,
|
|
||||||
but are just a starting point for custom validation with JavaScript.
|
|
||||||
|
|
||||||
## Model Validation
|
|
||||||
|
|
||||||
An alternative (or additional) approach to validation is to place it directly
|
|
||||||
on the model. SilverStripe provides a `[api:DataObject->validate()]` method for this purpose.
|
|
||||||
Refer to the ["datamodel" topic](/topics/datamodel#validation-and-constraints) for more information.
|
|
||||||
|
|
||||||
## Validation in the CMS
|
|
||||||
|
|
||||||
Since you're not creating the forms for editing CMS records,
|
|
||||||
SilverStripe provides you with a `getCMSValidator()` method on your models
|
|
||||||
to return a `[api:Validator]` instance.
|
|
||||||
|
|
||||||
:::php
|
|
||||||
class Page extends SiteTree {
|
|
||||||
private static $db = array('MyRequiredField' => 'Text');
|
|
||||||
|
|
||||||
public function getCMSValidator() {
|
|
||||||
return new RequiredFields(array('MyRequiredField'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
## Subclassing Validator
|
|
||||||
|
|
||||||
To create your own validator, you need to subclass validator and define two methods:
|
|
||||||
|
|
||||||
* **javascript()** Should output a snippet of JavaScript that will get called to perform javascript validation.
|
|
||||||
* **php($data)** Should return true if the given data is valid, and call $this->validationError() if there were any
|
|
||||||
errors.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
* Model Validation with [api:DataObject->validate()]
|
|
@ -1,6 +1,6 @@
|
|||||||
# Forms
|
# Forms
|
||||||
|
|
||||||
HTML forms are in practice the most used way to communicate with a browser.
|
HTML forms are in practice the most used way to interact with a user.
|
||||||
SilverStripe provides classes to generate and handle the actions and data from a
|
SilverStripe provides classes to generate and handle the actions and data from a
|
||||||
form.
|
form.
|
||||||
|
|
||||||
@ -9,14 +9,14 @@ form.
|
|||||||
A fully implemented form in SilverStripe includes a couple of classes that
|
A fully implemented form in SilverStripe includes a couple of classes that
|
||||||
individually have separate concerns.
|
individually have separate concerns.
|
||||||
|
|
||||||
* Controller - Takes care of assembling the form and receiving data from it.
|
* Controller—Takes care of assembling the form and receiving data from it.
|
||||||
* Form - Holds sets of fields, actions and validators.
|
* Form—Holds sets of fields, actions and validators.
|
||||||
* FormField - Fields that receive data or displays them, e.g input fields.
|
* FormField —Fields that receive data or displays them, e.g input fields.
|
||||||
* FormActions - Often submit buttons that executes actions.
|
* FormActions—Often submit buttons that executes actions.
|
||||||
* Validators - Validate the whole form, see [Form validation](form-validation.md) topic for more information.
|
* Validators—Validate the whole form.
|
||||||
|
|
||||||
Depending on your needs you can customize and override any of the above classes,
|
Depending on your needs you can customize and override any of the above classes;
|
||||||
however the defaults are often sufficient.
|
the defaults, however, are often sufficient.
|
||||||
|
|
||||||
## The Controller
|
## The Controller
|
||||||
|
|
||||||
@ -53,12 +53,13 @@ in a controller.
|
|||||||
The name of the form ("HelloForm") is passed into the `Form` constructor as a
|
The name of the form ("HelloForm") is passed into the `Form` constructor as a
|
||||||
second argument. It needs to match the method name.
|
second argument. It needs to match the method name.
|
||||||
|
|
||||||
Since forms need a URL, the `HelloForm()` method needs to be handled like any
|
Because forms need a URL, the `HelloForm()` method needs to be handled like any
|
||||||
other controller action. In order to whitelist its access through URLs, we add
|
other controller action. To grant it access through URLs, we add it to the
|
||||||
it to the `$allowed_actions` array.
|
`$allowed_actions` array.
|
||||||
|
|
||||||
Form actions ("doSayHello") on the other hand should NOT be included here, these
|
Form actions ("doSayHello"), on the other hand, should _not_ be included in
|
||||||
are handled separately through `Form->httpSubmission()`.
|
`$allowed_actions`; these are handled separately through
|
||||||
|
`Form->httpSubmission()`.
|
||||||
|
|
||||||
You can control access on form actions either by conditionally removing a
|
You can control access on form actions either by conditionally removing a
|
||||||
`FormAction` from the form construction, or by defining `$allowed_actions` in
|
`FormAction` from the form construction, or by defining `$allowed_actions` in
|
||||||
@ -68,19 +69,21 @@ your own `Form` class (more information in the
|
|||||||
**Page.ss**
|
**Page.ss**
|
||||||
|
|
||||||
:::ss
|
:::ss
|
||||||
<!-- place where you would like the form to show up -->
|
<%-- place where you would like the form to show up --%>
|
||||||
<div>$HelloForm</div>
|
<div>$HelloForm</div>
|
||||||
|
|
||||||
<div class="warning" markdown='1'>
|
<div class="warning" markdown='1'>
|
||||||
Be sure to add the Form name 'HelloForm' to the Controller::$allowed_actions()
|
Be sure to add the Form name 'HelloForm' to your controller's $allowed_actions
|
||||||
to be sure that form submissions get through to the correct action.
|
array to enable form submissions.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notice" markdown='1'>
|
<div class="notice" markdown='1'>
|
||||||
You'll notice that we've used a new notation for creating form fields, using `create()` instead of the `new` operator.
|
You'll notice that we've used a new notation for creating form fields, using
|
||||||
These are functionally equivalent, but allows PHP to chain operations like `setTitle()` without assigning the field
|
`create()` instead of the `new` operator. These are functionally equivalent, but
|
||||||
instance to a temporary variable. For in-depth information on the create syntax, see the [Injector](/reference/injector)
|
allows PHP to chain operations like `setTitle()` without assigning the field
|
||||||
documentation or the API documentation for `[api:Object]`::create().
|
instance to a temporary variable. For in-depth information on the create syntax,
|
||||||
|
see the [Injector](/reference/injector) documentation or the API documentation
|
||||||
|
for `[api:Object]`::create().
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## The Form
|
## The Form
|
||||||
@ -95,12 +98,17 @@ Creating a form is a matter of defining a method to represent that form. This
|
|||||||
method should return a form object. The constructor takes the following
|
method should return a form object. The constructor takes the following
|
||||||
arguments:
|
arguments:
|
||||||
|
|
||||||
* `$controller`: This must be an instance of the controller that contains the form, often `$this`.
|
* `$controller`: This must be an instance of the controller that contains the
|
||||||
* `$name`: This must be the name of the method on that controller that is called to return the form. The first two
|
form, often `$this`.
|
||||||
fields allow the form object to be re-created after submission. **It's vital that they are properly set - if you ever
|
* `$name`: This must be the name of the method on that controller that is
|
||||||
have problems with form action handler not working, check that these values are correct.**
|
called to return the form. The first two arguments allow the form object
|
||||||
* `$fields`: A `[api:FieldList]` containing `[api:FormField]` instances make up fields in the form.
|
to be re-created after submission. **It's vital that they be properly
|
||||||
* `$actions`: A `[api:FieldList]` containing the `[api:FormAction]` objects - the buttons at the bottom.
|
set—if you ever have problems with a form action handler not working,
|
||||||
|
check that these values are correct.**
|
||||||
|
* `$fields`: A `[api:FieldList]` containing `[api:FormField]` instances make
|
||||||
|
up fields in the form.
|
||||||
|
* `$actions`: A `[api:FieldList]` containing the `[api:FormAction]` objects -
|
||||||
|
the buttons at the bottom.
|
||||||
* `$validator`: An optional `[api:Validator]` for validation of the form.
|
* `$validator`: An optional `[api:Validator]` for validation of the form.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -119,13 +127,14 @@ Example:
|
|||||||
|
|
||||||
## Subclassing a form
|
## Subclassing a form
|
||||||
|
|
||||||
It's the responsibility of your subclass' constructor to call
|
It's the responsibility of your subclass's constructor to call
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
parent::__construct()
|
parent::__construct()
|
||||||
|
|
||||||
with the right parameters. You may choose to take $fields and $actions as arguments if you wish, but $controller and
|
with the right parameters. You may choose to take $fields and $actions as
|
||||||
$name must be passed - their values depend on where the form is instantiated.
|
arguments if you wish, but $controller and $name must be passed—their values
|
||||||
|
depend on where the form is instantiated.
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
class MyForm extends Form {
|
class MyForm extends Form {
|
||||||
@ -141,8 +150,9 @@ $name must be passed - their values depend on where the form is instantiated.
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
The real difference, however, is that you can then define your controller methods within the form class itself. This
|
The real difference, however, is that you can then define your controller
|
||||||
means that the form takes responsibilities from the controller and manage how to parse and use the form
|
methods within the form class itself. This means that the form takes
|
||||||
|
responsibilities from the controller and manage how to parse and use the form
|
||||||
data.
|
data.
|
||||||
|
|
||||||
**Page.php**
|
**Page.php**
|
||||||
@ -214,7 +224,7 @@ form.
|
|||||||
## Readonly
|
## Readonly
|
||||||
|
|
||||||
You can turn a form or individual fields into a readonly version. This is handy
|
You can turn a form or individual fields into a readonly version. This is handy
|
||||||
in the case of confirmation pages or when certain fields can be edited due to
|
in the case of confirmation pages or when certain fields cannot be edited due to
|
||||||
permissions.
|
permissions.
|
||||||
|
|
||||||
Readonly on a Form
|
Readonly on a Form
|
||||||
@ -241,10 +251,13 @@ Readonly on a FormField
|
|||||||
|
|
||||||
You can use a custom form template to render with, instead of *Form.ss*
|
You can use a custom form template to render with, instead of *Form.ss*
|
||||||
|
|
||||||
It's recommended you only do this if you've got a lot of presentation text, graphics that surround the form fields. This
|
It's recommended you do this only if you have a lot of presentation text or
|
||||||
is better than defining those as *LiteralField* objects, as it doesn't clutter the data layer with presentation junk.
|
graphics that surround the form fields. This is better than defining those as
|
||||||
|
*LiteralField* objects, as it doesn't clutter the data layer with presentation
|
||||||
|
junk.
|
||||||
|
|
||||||
First of all, you need to create your form on it's own class, that way you can define a custom template using a `forTemplate()` method on your Form class.
|
First you need to create your own form class extending Form; that way you can
|
||||||
|
define a custom template using a `forTemplate()` method on your Form class.
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
class MyForm extends Form {
|
class MyForm extends Form {
|
||||||
@ -305,14 +318,16 @@ your project. Here is an example of basic customization:
|
|||||||
<% end_if %>
|
<% end_if %>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
`$Fields.dataFieldByName(FirstName)` will return the form control contents of `Field()` for the particular field object,
|
`$Fields.dataFieldByName(FirstName)` will return the form control contents of
|
||||||
in this case `EmailField->Field()` or `PasswordField->Field()` which returns an `<input>` element with specific markup
|
`Field()` for the particular field object, in this case `EmailField->Field()` or
|
||||||
for the type of field. Pass in the name of the field as the first parameter, as done above, to render it into the
|
`PasswordField->Field()` which returns an `<input>` element with specific markup
|
||||||
template.
|
for the type of field. Pass in the name of the field as the first parameter, as
|
||||||
|
done above, to render it into the template.
|
||||||
|
|
||||||
To find more methods, have a look at the `[api:Form]` class and `[api:FieldList]` class as there is a lot of different
|
To find more methods, have a look at the `[api:Form]` class and
|
||||||
methods of customising the form templates. An example is that you could use `<% loop $Fields %>` instead of specifying
|
`[api:FieldList]` class as there is a lot of different methods of customising
|
||||||
each field manually, as we've done above.
|
the form templates. An example is that you could use `<% loop $Fields %>`
|
||||||
|
instead of specifying each field manually, as we've done above.
|
||||||
|
|
||||||
### Custom form field templates
|
### Custom form field templates
|
||||||
|
|
||||||
@ -333,19 +348,20 @@ Each form field is rendered into a form via the
|
|||||||
`<div>` as well as a `<label>` element (if applicable).
|
`<div>` as well as a `<label>` element (if applicable).
|
||||||
|
|
||||||
You can also render each field without these structural elements through the
|
You can also render each field without these structural elements through the
|
||||||
`[FormField->Field()](api:FormField)` method. In order to influence the form
|
`[FormField->Field()](api:FormField)` method. To influence form rendering,
|
||||||
rendering, overloading these two methods is a good start.
|
overriding these two methods is a good start.
|
||||||
|
|
||||||
In addition, most form fields are rendered through SilverStripe templates, e.g.
|
In addition, most form fields are rendered through SilverStripe templates; for
|
||||||
`TextareaField` is rendered via `framework/templates/forms/TextareaField.ss`.
|
example, `TextareaField` is rendered via
|
||||||
|
`framework/templates/forms/TextareaField.ss`.
|
||||||
|
|
||||||
These templates can be overwritten globally by placing a template with the same
|
These templates can be overridden globally by placing a template with the same
|
||||||
name in your `mysite` directory, or set on a form field instance via anyone of
|
name in your `mysite` directory, or set on a form field instance via any of
|
||||||
these methods:
|
these methods:
|
||||||
|
|
||||||
- FormField->setTemplate()
|
- FormField->setTemplate()
|
||||||
- FormField->setFieldHolderTemplate()
|
- FormField->setFieldHolderTemplate()
|
||||||
- FormField->getSmallFieldHolderTemplate()
|
- FormField->getSmallFieldHolderTemplate()
|
||||||
|
|
||||||
<div class="hint" markdown='1'>
|
<div class="hint" markdown='1'>
|
||||||
Caution: Not all FormFields consistently uses templates set by the above methods.
|
Caution: Not all FormFields consistently uses templates set by the above methods.
|
||||||
@ -358,7 +374,7 @@ by adding a hidden *SecurityID* parameter to each form. See
|
|||||||
[secure-development](/topics/security) for details.
|
[secure-development](/topics/security) for details.
|
||||||
|
|
||||||
In addition, you should limit forms to the intended HTTP verb (mostly `GET` or `POST`)
|
In addition, you should limit forms to the intended HTTP verb (mostly `GET` or `POST`)
|
||||||
to further reduce attack surface, by using `[api:Form->setStrictFormMethodCheck()]`.
|
to further reduce attack exposure, by using `[api:Form->setStrictFormMethodCheck()]`.
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
$myForm->setFormMethod('POST');
|
$myForm->setFormMethod('POST');
|
||||||
@ -391,10 +407,170 @@ Adds a new text field called FavouriteColour next to the Content field in the CM
|
|||||||
:::php
|
:::php
|
||||||
$this->Fields()->addFieldToTab('Root.Content', new TextField('FavouriteColour'), 'Content');
|
$this->Fields()->addFieldToTab('Root.Content', new TextField('FavouriteColour'), 'Content');
|
||||||
|
|
||||||
|
## Form Validation
|
||||||
|
|
||||||
|
SilverStripe provides PHP form validation out of the box, but doesn't come with
|
||||||
|
any built-in JavaScript validation (the previously used `Validator.js` approach
|
||||||
|
has been deprecated).
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
Validators are implemented as an argument to the `[api:Form]` constructor, and
|
||||||
|
are subclasses of the abstract `[api:Validator]` base class. The only
|
||||||
|
implementation that comes with SilverStripe is the `[api:RequiredFields]` class,
|
||||||
|
which ensures that fields are filled out when the form is submitted.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
public function Form() {
|
||||||
|
$form = new Form($this, 'Form',
|
||||||
|
new FieldList(
|
||||||
|
new TextField('MyRequiredField'),
|
||||||
|
new TextField('MyOptionalField')
|
||||||
|
),
|
||||||
|
new FieldList(
|
||||||
|
new FormAction('submit', 'Submit form')
|
||||||
|
),
|
||||||
|
new RequiredFields(array('MyRequiredField'))
|
||||||
|
);
|
||||||
|
// Optional: Add a CSS class for custom styling
|
||||||
|
$form->dataFieldByName('MyRequiredField')->addExtraClass('required');
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
### Form Field Validation
|
||||||
|
|
||||||
|
Form fields are responsible for validating the data they process, through the
|
||||||
|
`[api:FormField->validate()]` method. There are many fields for different
|
||||||
|
purposes (see ["form field types"](/reference/form-field-types) for a full list).
|
||||||
|
|
||||||
|
### Adding your own validation messages
|
||||||
|
|
||||||
|
In many cases, you want to add PHP validation that is more complex than
|
||||||
|
validating the format or existence of a single form field input. For example,
|
||||||
|
you might want to have dependent validation on a postcode which depends on the
|
||||||
|
country you've selected in a different field.
|
||||||
|
|
||||||
|
There are two ways to go about this: attach a custom error message to a specific
|
||||||
|
field, or a generic message to the whole form.
|
||||||
|
|
||||||
|
Example: Validate postcodes based on the selected country (on the controller).
|
||||||
|
|
||||||
|
:::php
|
||||||
|
class MyController extends Controller {
|
||||||
|
private static $allowed_actions = array('Form');
|
||||||
|
public function Form() {
|
||||||
|
return Form::create($this, 'Form',
|
||||||
|
new FieldList(
|
||||||
|
new NumericField('Postcode'),
|
||||||
|
new CountryDropdownField('Country')
|
||||||
|
),
|
||||||
|
new FieldList(
|
||||||
|
new FormAction('submit', 'Submit form')
|
||||||
|
),
|
||||||
|
new RequiredFields(array('Country'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public function submit($data, $form) {
|
||||||
|
// At this point, RequiredFields->validate() will have been called already,
|
||||||
|
// so we can assume that the values exist.
|
||||||
|
|
||||||
|
// German postcodes need to be five digits
|
||||||
|
if($data['Country'] == 'de' && isset($data['Postcode']) && strlen($data['Postcode']) != 5) {
|
||||||
|
$form->addErrorMessage('Postcode', 'Need five digits for German postcodes', 'bad');
|
||||||
|
return $this->redirectBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global validation error (not specific to form field)
|
||||||
|
if($data['Country'] == 'IR' && isset($data['Postcode']) && $data['Postcode']) {
|
||||||
|
$form->sessionMessage("Ireland doesn't have postcodes!", 'bad');
|
||||||
|
return $this->redirectBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue normal processing...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### JavaScript Validation
|
||||||
|
|
||||||
|
Although there are no built-in JavaScript validation handlers in SilverStripe,
|
||||||
|
the `FormField` API is flexible enough to provide the information required in
|
||||||
|
order to plug in custom libraries.
|
||||||
|
|
||||||
|
#### HTML5 attributes
|
||||||
|
|
||||||
|
HTML5 specifies some built-in form validations
|
||||||
|
([source](http://www.w3.org/wiki/HTML5_form_additions)), which are evaluated by
|
||||||
|
modern browsers without any need for JavaScript. SilverStripe supports this by
|
||||||
|
allowing to set custom attributes on fields.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Markup contains <input type="text" required />
|
||||||
|
TextField::create('MyText')->setAttribute('required', true);
|
||||||
|
|
||||||
|
// Markup contains <input type="url" pattern="https?://.+" />
|
||||||
|
TextField::create('MyText')
|
||||||
|
->setAttribute('type', 'url')
|
||||||
|
->setAttribute('pattern', 'https?://.+')
|
||||||
|
|
||||||
|
#### HTML5 metadata
|
||||||
|
|
||||||
|
In addition, HTML5 elements can contain custom data attributes with the `data-`
|
||||||
|
prefix. These are general-purpose attributes, but can be used to hook in your
|
||||||
|
own validation.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// Validate a specific date format (in PHP)
|
||||||
|
// Markup contains <input type="text" data-dateformat="dd.MM.yyyy" />
|
||||||
|
DateField::create('MyDate')->setConfig('dateformat', 'dd.MM.yyyy');
|
||||||
|
|
||||||
|
// Limit extensions on upload (in PHP)
|
||||||
|
// Markup contains <input type="file" data-allowed-extensions="jpg,jpeg,gif" />
|
||||||
|
$exts = array('jpg', 'jpeg', 'gif');
|
||||||
|
$validator = new Upload_Validator();
|
||||||
|
$validator->setAllowedExtensions($exts);
|
||||||
|
$upload = Upload::create()->setValidator($validator);
|
||||||
|
$fileField = FileField::create('MyFile')->setUpload(new);
|
||||||
|
$fileField->setAttribute('data-allowed-extensions', implode(',', $exts));
|
||||||
|
|
||||||
|
Note that these examples don't have any effect on the client as such, but are
|
||||||
|
just a starting point for custom validation with JavaScript.
|
||||||
|
|
||||||
|
### Model Validation
|
||||||
|
|
||||||
|
An alternative (or additional) approach to validation is to place it directly on
|
||||||
|
the model. SilverStripe provides a `[api:DataObject->validate()]` method for
|
||||||
|
this purpose. Refer to the
|
||||||
|
["datamodel" topic](/topics/datamodel#validation-and-constraints) for more information.
|
||||||
|
|
||||||
|
### Validation in the CMS
|
||||||
|
|
||||||
|
Since you're not creating the forms for editing CMS records, SilverStripe
|
||||||
|
provides you with a `getCMSValidator()` method on your models to return a
|
||||||
|
`[api:Validator]` instance.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
class Page extends SiteTree {
|
||||||
|
private static $db = array('MyRequiredField' => 'Text');
|
||||||
|
|
||||||
|
public function getCMSValidator() {
|
||||||
|
return new RequiredFields(array('MyRequiredField'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### Subclassing Validator
|
||||||
|
|
||||||
|
To create your own validator, you need to subclass validator and define two methods:
|
||||||
|
|
||||||
|
* **javascript()** Should output a snippet of JavaScript that will get called
|
||||||
|
to perform javascript validation.
|
||||||
|
* **php($data)** Should return true if the given data is valid, and call
|
||||||
|
$this->validationError() if there were any errors.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
* [Form Field Types](/reference/form-field-types)
|
* [Form Field Types](/reference/form-field-types)
|
||||||
* [MultiForm Module](http://silverstripe.org/multi-form-module)
|
* [MultiForm Module](http://silverstripe.org/multi-form-module)
|
||||||
|
* Model Validation with [api:DataObject->validate()]
|
||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
|
@ -16,18 +16,19 @@ It is where most documentation should live, and is the natural "second step" aft
|
|||||||
* [Environment management](environment-management): Sharing configuration details (e.g. database login, passwords) with multiple websites via a `_ss_environment.php` file
|
* [Environment management](environment-management): Sharing configuration details (e.g. database login, passwords) with multiple websites via a `_ss_environment.php` file
|
||||||
* [Error Handling](error-handling): Error messages and filesystem logs
|
* [Error Handling](error-handling): Error messages and filesystem logs
|
||||||
* [Files and Images](files): File and Image management in the database and how to manipulate images
|
* [Files and Images](files): File and Image management in the database and how to manipulate images
|
||||||
* [Form Validation](form-validation): Built-in validation on form fields, and how to extend it
|
* [Forms & form validation](forms): Create your own form, add fields and create your own form template using the existing `Form` class
|
||||||
* [Forms](forms): Create your own form, add fields and create your own form template using the existing `Form` class
|
|
||||||
* [Internationalization (i18n)](i18n): Displaying templates and PHP code in different languages using i18n
|
* [Internationalization (i18n)](i18n): Displaying templates and PHP code in different languages using i18n
|
||||||
* [Javascript](javascript): Best practices for developing with JavaScript in SilverStripe
|
* [Javascript](javascript): Best practices for developing with JavaScript in SilverStripe
|
||||||
* [Module Development](module-development): Creating a module (also known as "extension" or "plugin") to contain reusable functionality
|
* [Module Development](module-development): Creating a module (also known as "extension" or "plugin") to contain reusable functionality
|
||||||
* [Modules](modules): Introduction, how to download and install a module (e.g. with blog or forum functionality)
|
* [Modules](modules): Introduction, how to download and install a module (e.g. with blog or forum functionality)
|
||||||
|
* [Page Type Templates](page-type-templates): How to build templates for all your different page types
|
||||||
* [Page Types](page-types): What is a "page type" and how do you create one?
|
* [Page Types](page-types): What is a "page type" and how do you create one?
|
||||||
|
* [Rich Text Editing](rich-text-editing): How to use and configure SilverStripes built in HTML Editor
|
||||||
* [Search](search): Searching for properties in the database as well as other documents
|
* [Search](search): Searching for properties in the database as well as other documents
|
||||||
* [Security](security): How to develop secure SilverStripe applications with good code examples
|
* [Security](security): How to develop secure SilverStripe applications with good code examples
|
||||||
* [Shortcodes](shortcodes): Use simple placeholders for powerful content replacements like multimedia embeds
|
|
||||||
* [Templates](templates): SilverStripe template syntax: Variables, loops, includes and much more
|
* [Templates](templates): SilverStripe template syntax: Variables, loops, includes and much more
|
||||||
* [Testing](testing): Functional and Unit Testing with PHPUnit and SilverStripe's testing framework
|
* [Testing](testing): Functional and Unit Testing with PHPUnit and SilverStripe's testing framework
|
||||||
* [Developing Themes](theme-development): Package templates, images and CSS to a reusable theme
|
* [Theme Development](theme-development): Package templates, images and CSS to a reusable theme
|
||||||
* [Widgets](widgets): Small feature blocks which can be placed on a page by the CMS editor, also outlines how to create and add widgets
|
* [Using Themes](themes): How to download and install themes
|
||||||
* [Versioning](versioning): Extension for SiteTree and other classes to store old versions and provide "staging"
|
* [Versioning](versioning): Extension for SiteTree and other classes to store old versions and provide "staging"
|
||||||
|
* [Widgets](widgets): Small feature blocks which can be placed on a page by the CMS editor, also outlines how to create and add widgets
|
||||||
|
@ -2,38 +2,42 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
You will often find the need to test your functionality with some consistent data.
|
You will often find the need to test your functionality with some consistent
|
||||||
If we are testing our code with the same data each time,
|
data. If we are testing our code with the same data each time, we can trust our
|
||||||
we can trust our tests to yeild reliable results.
|
tests to yield reliable results.
|
||||||
In Silverstripe we define this data via 'fixtures' (so called because of their fixed nature).
|
|
||||||
The `[api:SapphireTest]` class takes care of populating a test database with data from these fixtures -
|
In Silverstripe we define this data via 'fixtures' (so called because of their
|
||||||
all we have to do is define them, and we have a few ways in which we can do this.
|
fixed nature). The `[api:SapphireTest]` class takes care of populating a test
|
||||||
|
database with data from these fixtures - all we have to do is define them, and
|
||||||
|
we have a few ways in which we can do this.
|
||||||
|
|
||||||
## YAML Fixtures
|
## YAML Fixtures
|
||||||
|
|
||||||
YAML is a markup language which is deliberately simple and easy to read,
|
YAML is a markup language which is deliberately simple and easy to read, so it
|
||||||
so it is ideal for fixture generation.
|
is ideal for fixture generation.
|
||||||
|
|
||||||
Say we have the following two DataObjects:
|
Say we have the following two DataObjects:
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
class Player extends DataObject {
|
class Player extends DataObject {
|
||||||
static $db = array (
|
|
||||||
|
private static $db = array (
|
||||||
'Name' => 'Varchar(255)'
|
'Name' => 'Varchar(255)'
|
||||||
);
|
);
|
||||||
|
|
||||||
static $has_one = array(
|
private static $has_one = array(
|
||||||
'Team' => 'Team'
|
'Team' => 'Team'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Team extends DataObject {
|
class Team extends DataObject {
|
||||||
static $db = array (
|
|
||||||
|
private static $db = array (
|
||||||
'Name' => 'Varchar(255)',
|
'Name' => 'Varchar(255)',
|
||||||
'Origin' => 'Varchar(255)'
|
'Origin' => 'Varchar(255)'
|
||||||
);
|
);
|
||||||
|
|
||||||
static $has_many = array(
|
private static $has_many = array(
|
||||||
'Players' => 'Player'
|
'Players' => 'Player'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -59,31 +63,42 @@ We can represent multiple instances of them in `YAML` as follows:
|
|||||||
Name: The Crusaders
|
Name: The Crusaders
|
||||||
Origin: Bay of Plenty
|
Origin: Bay of Plenty
|
||||||
|
|
||||||
Our `YAML` is broken up into three levels, signified by the indentation of each line.
|
Our `YAML` is broken up into three levels, signified by the indentation of each
|
||||||
In the first level of indentation, `Player` and `Team`,
|
line. In the first level of indentation, `Player` and `Team`, represent the
|
||||||
represent the class names of the objects we want to be created for the test.
|
class names of the objects we want to be created for the test.
|
||||||
|
|
||||||
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
|
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are
|
||||||
These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
|
identifiers. These are what you pass as the second argument of
|
||||||
Each identifier you specify represents a new object.
|
`SapphireTest::objFromFixture()`. Each identifier you specify represents a new
|
||||||
|
object.
|
||||||
|
|
||||||
The third and final level represents each individual object's fields.
|
The third and final level represents each individual object's fields.
|
||||||
A field can either be provided with raw data (such as the Names for our Players),
|
|
||||||
or we can define a relationship, as seen by the fields prefixed with `=>`.
|
|
||||||
|
|
||||||
Each one of our Players has a relationship to a Team,
|
A field can either be provided with raw data (such as the names for our
|
||||||
this is shown with the `Team` field for each `Player` being set to `=>Team.` followed by a team name.
|
Players), or we can define a relationship, as seen by the fields prefixed with
|
||||||
Take the player John for example, his team is the Hurricanes which is represented by `=>Team.hurricanes`.
|
`=>`.
|
||||||
This is tells the system that we want to set up a relationship for the `Player` object `john` with the `Team` object `hurricanes`.
|
|
||||||
|
Each one of our Players has a relationship to a Team, this is shown with the
|
||||||
|
`Team` field for each `Player` being set to `=>Team.` followed by a team name.
|
||||||
|
|
||||||
|
Take the player John for example, his team is the Hurricanes which is
|
||||||
|
represented by `=>Team.hurricanes`.
|
||||||
|
|
||||||
|
This is tells the system that we want to set up a relationship for the `Player`
|
||||||
|
object `john` with the `Team` object `hurricanes`.
|
||||||
|
|
||||||
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
|
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
|
||||||
just like how a relationship is always set up.
|
just like how a relationship is always set up.
|
||||||
|
|
||||||
<div class="hint" markdown='1'>
|
<div class="hint" markdown='1'>
|
||||||
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID).
|
Note that we use the name of the relationship (Team), and not the name of the
|
||||||
|
database field (TeamID).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
|
This style of relationship declaration can be used for both a `has-one` and a
|
||||||
For `many-many` relationships, we specify a comma separated list of values.
|
`many-many` relationship. For `many-many` relationships, we specify a comma
|
||||||
|
separated list of values.
|
||||||
|
|
||||||
For example we could just as easily write the above as:
|
For example we could just as easily write the above as:
|
||||||
|
|
||||||
:::yml
|
:::yml
|
||||||
@ -104,27 +119,97 @@ For example we could just as easily write the above as:
|
|||||||
Origin: Bay of Plenty
|
Origin: Bay of Plenty
|
||||||
Players: =>Player.joe,=>Player.jack
|
Players: =>Player.joe,=>Player.jack
|
||||||
|
|
||||||
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**.
|
A crucial thing to note is that **the YAML file specifies DataObjects, not
|
||||||
The database is populated by instantiating DataObject objects and setting the fields declared in the YML,
|
database records**.
|
||||||
then calling write() on those objects.
|
|
||||||
This means that any `onBeforeWrite()` or default value logic will be executed as part of the test.
|
The database is populated by instantiating DataObject objects and setting the
|
||||||
The reasoning behind this is to allow us to test the `onBeforeWrite` functionality of our objects.
|
fields declared in the YML, then calling write() on those objects. This means
|
||||||
You can see this kind of testing in action in the `testURLGeneration()` test from the example in
|
that any `onBeforeWrite()` or default value logic will be executed as part of
|
||||||
[Creating a SilverStripe Test](creating-a-silverstripe-test).
|
the test. The reasoning behind this is to allow us to test the `onBeforeWrite`
|
||||||
|
functionality of our objects.
|
||||||
|
|
||||||
|
You can see this kind of testing in action in the `testURLGeneration()` test
|
||||||
|
from the example in [Creating a SilverStripe Test](creating-a-silverstripe-test).
|
||||||
|
|
||||||
|
### Defining many_many_extraFields
|
||||||
|
|
||||||
|
`many_many` relations can have additional database fields attached to the
|
||||||
|
relationship. For example we may want to declare the role each player has in the
|
||||||
|
team.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
class Player extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array (
|
||||||
|
'Name' => 'Varchar(255)'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $belongs_many_many = array(
|
||||||
|
'Teams' => 'Team'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Team extends DataObject {
|
||||||
|
|
||||||
|
private static $db = array (
|
||||||
|
'Name' => 'Varchar(255)'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $many_many = array(
|
||||||
|
'Players' => 'Player'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $many_many_extraFields = array(
|
||||||
|
"Players" => array(
|
||||||
|
"Role" => "Varchar"
|
||||||
|
);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
To provide the value for the many_many_extraField use the YAML list syntax.
|
||||||
|
|
||||||
|
:::yml
|
||||||
|
Player:
|
||||||
|
john:
|
||||||
|
Name: John
|
||||||
|
joe:
|
||||||
|
Name: Joe
|
||||||
|
jack:
|
||||||
|
Name: Jack
|
||||||
|
Team:
|
||||||
|
hurricanes:
|
||||||
|
Name: The Hurricanes
|
||||||
|
Players:
|
||||||
|
- =>Player.john:
|
||||||
|
Role: Captain
|
||||||
|
|
||||||
|
crusaders:
|
||||||
|
Name: The Crusaders
|
||||||
|
Players:
|
||||||
|
- =>Player.joe:
|
||||||
|
Role: Captain
|
||||||
|
- =>Player.jack:
|
||||||
|
Role: Winger
|
||||||
|
|
||||||
## Test Class Definition
|
## Test Class Definition
|
||||||
|
|
||||||
### Manual Object Creation
|
### Manual Object Creation
|
||||||
|
|
||||||
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
|
Sometimes statically defined fixtures don't suffice. This could be because of
|
||||||
or because the YAML format doesn't allow you to modify all of a model's state.
|
the complexity of the tested model, or because the YAML format doesn't allow you
|
||||||
One common example here is publishing pages (page fixtures aren't published by default).
|
to modify all of a model's state.
|
||||||
|
|
||||||
|
One common example here is publishing pages (page fixtures aren't published by
|
||||||
|
default).
|
||||||
|
|
||||||
You can always resort to creating objects manually in the test setup phase.
|
You can always resort to creating objects manually in the test setup phase.
|
||||||
Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
|
|
||||||
|
Since the test database is cleared on every test method, you'll get a fresh set
|
||||||
|
of test instances every time.
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
class SiteTreeTest extends SapphireTest {
|
class SiteTreeTest extends SapphireTest {
|
||||||
|
|
||||||
function setUp() {
|
function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
@ -140,16 +225,20 @@ Since the test database is cleared on every test method, you'll get a fresh set
|
|||||||
|
|
||||||
### Why Factories?
|
### Why Factories?
|
||||||
|
|
||||||
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
|
While manually defined fixtures provide full flexibility, they offer very little
|
||||||
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
|
in terms of structure and convention. Alternatively, you can use the
|
||||||
callbacks on object creation, and dynamic/lazy value setting.
|
`[api:FixtureFactory]` class, which allows you to set default values, callbacks
|
||||||
|
on object creation, and dynamic/lazy value setting.
|
||||||
|
|
||||||
<div class="hint" markdown='1'>
|
<div class="hint" markdown='1'>
|
||||||
SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures.
|
SapphireTest uses FixtureFactory under the hood when it is provided with YAML
|
||||||
|
based fixtures.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
The idea is that rather than instantiating objects directly, we'll have a factory class for them.
|
The idea is that rather than instantiating objects directly, we'll have a
|
||||||
This factory can have so called "blueprints" defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a name, which is usually set to the class it creates.
|
factory class for them. This factory can have so called "blueprints" defined on
|
||||||
|
it, which tells the factory how to instantiate an object of a specific type.
|
||||||
|
Blueprints need a name, which is usually set to the class it creates.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ When designing your site you should only need to modify the *mysite*, *themes* a
|
|||||||
|
|
||||||
![](_images/tutorial1_cms-basic.jpg)
|
![](_images/tutorial1_cms-basic.jpg)
|
||||||
|
|
||||||
The CMS is the area in which you can manage your site content. You can access the cms at http://localhost/your_site_name/admin (or http://yourdomain.com/admin if you are using you own domain name). You
|
The CMS is the area in which you can manage your site content. You can access the cms at http://localhost/your_site_name/admin (or http://yourdomain.com/admin if you are using your own domain name). You
|
||||||
will be presented with a login screen. Login using the details you provided at installation. After logging in you
|
will be presented with a login screen. Login using the details you provided at installation. After logging in you
|
||||||
should see the CMS interface with a list of the pages currently on your website (the site tree). Here you can add, delete and reorganize pages. If you need to delete, publish, or unpublish a page, first check "multi-selection" at the top. You will then be able to perform actions on any checked files using the "Actions" dropdown. Clicking on a page will open it in the page editing interface pictured below (we've entered some test content).
|
should see the CMS interface with a list of the pages currently on your website (the site tree). Here you can add, delete and reorganize pages. If you need to delete, publish, or unpublish a page, first check "multi-selection" at the top. You will then be able to perform actions on any checked files using the "Actions" dropdown. Clicking on a page will open it in the page editing interface pictured below (we've entered some test content).
|
||||||
|
|
||||||
|
@ -732,7 +732,7 @@ class File extends DataObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does not change the filesystem itself, please use {@link write()} for this.
|
* Caution: this does not change the location of the file on the filesystem.
|
||||||
*/
|
*/
|
||||||
public function setFilename($val) {
|
public function setFilename($val) {
|
||||||
$this->setField('Filename', $val);
|
$this->setField('Filename', $val);
|
||||||
|
@ -57,20 +57,23 @@ class Folder extends File {
|
|||||||
|
|
||||||
$parentID = 0;
|
$parentID = 0;
|
||||||
$item = null;
|
$item = null;
|
||||||
|
$filter = FileNameFilter::create();
|
||||||
foreach($parts as $part) {
|
foreach($parts as $part) {
|
||||||
if(!$part) continue; // happens for paths with a trailing slash
|
if(!$part) continue; // happens for paths with a trailing slash
|
||||||
$item = DataObject::get_one(
|
|
||||||
"Folder",
|
// Ensure search includes folders with illegal characters removed, but
|
||||||
sprintf(
|
// err in favour of matching existing folders if $folderPath
|
||||||
"\"Name\" = '%s' AND \"ParentID\" = %d",
|
// includes illegal characters itself.
|
||||||
Convert::raw2sql($part),
|
$partSafe = $filter->filter($part);
|
||||||
(int)$parentID
|
$item = Folder::get()->filter(array(
|
||||||
)
|
'ParentID' => $parentID,
|
||||||
);
|
'Name' => array($partSafe, $part)
|
||||||
|
))->first();
|
||||||
|
|
||||||
if(!$item) {
|
if(!$item) {
|
||||||
$item = new Folder();
|
$item = new Folder();
|
||||||
$item->ParentID = $parentID;
|
$item->ParentID = $parentID;
|
||||||
$item->Name = $part;
|
$item->Name = $partSafe;
|
||||||
$item->Title = $part;
|
$item->Title = $part;
|
||||||
$item->write();
|
$item->write();
|
||||||
}
|
}
|
||||||
@ -460,7 +463,7 @@ class Folder extends File {
|
|||||||
* Get the children of this folder that are also folders.
|
* Get the children of this folder that are also folders.
|
||||||
*/
|
*/
|
||||||
public function ChildFolders() {
|
public function ChildFolders() {
|
||||||
return DataObject::get("Folder", "\"ParentID\" = " . (int)$this->ID);
|
return Folder::get()->filter('ParentID', $this->ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -134,19 +134,24 @@ class Upload extends Controller {
|
|||||||
$file = $nameFilter->filter($tmpFile['name']);
|
$file = $nameFilter->filter($tmpFile['name']);
|
||||||
$fileName = basename($file);
|
$fileName = basename($file);
|
||||||
|
|
||||||
$relativeFilePath = $parentFolder->getRelativePath() . "/$fileName";
|
$relativeFilePath = $parentFolder ? $parentFolder->getRelativePath() . "$fileName" : $fileName;
|
||||||
|
|
||||||
// Create a new file record (or try to retrieve an existing one)
|
// Create a new file record (or try to retrieve an existing one)
|
||||||
if(!$this->file) {
|
if(!$this->file) {
|
||||||
$fileClass = File::get_class_for_file_extension(pathinfo($tmpFile['name'], PATHINFO_EXTENSION));
|
$fileClass = File::get_class_for_file_extension(pathinfo($tmpFile['name'], PATHINFO_EXTENSION));
|
||||||
if($this->replaceFile) {
|
$this->file = new $fileClass();
|
||||||
$this->file = File::get()
|
}
|
||||||
|
if(!$this->file->ID && $this->replaceFile) {
|
||||||
|
$fileClass = $this->file->class;
|
||||||
|
$file = File::get()
|
||||||
->filter(array(
|
->filter(array(
|
||||||
|
'ClassName' => $fileClass,
|
||||||
'Name' => $fileName,
|
'Name' => $fileName,
|
||||||
'ParentID' => $parentFolder ? $parentFolder->ID : 0
|
'ParentID' => $parentFolder ? $parentFolder->ID : 0
|
||||||
))->First();
|
))->First();
|
||||||
|
if($file) {
|
||||||
|
$this->file = $file;
|
||||||
}
|
}
|
||||||
if(!$this->file) $this->file = new $fileClass();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if filename already exists, version the filename (e.g. test.gif to test1.gif)
|
// if filename already exists, version the filename (e.g. test.gif to test1.gif)
|
||||||
|
@ -117,15 +117,15 @@ class DropdownField extends FormField {
|
|||||||
protected $disabledItems = array();
|
protected $disabledItems = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new dropdown field.
|
* @param string $name The field name
|
||||||
* @param $name The field name
|
* @param string $title The field title
|
||||||
* @param $title The field title
|
* @param array $source An map of the dropdown items
|
||||||
* @param $source An map of the dropdown items
|
* @param string $value The current value
|
||||||
* @param $value The current value
|
* @param Form $form The parent form
|
||||||
* @param $form The parent form
|
* @param string|bool $emptyString Add an empty selection on to of the {@link $source}-Array (can also be
|
||||||
* @param $emptyString mixed Add an empty selection on to of the {@link $source}-Array (can also be boolean, which
|
* boolean, which results in an empty string). Argument is deprecated
|
||||||
* results in an empty string). Argument is deprecated in 3.1, please use
|
* in 3.1, please use{@link setEmptyString()} and/or
|
||||||
* {@link setEmptyString()} and/or {@link setHasEmptyDefault(true)} instead.
|
* {@link setHasEmptyDefault(true)} instead.
|
||||||
*/
|
*/
|
||||||
public function __construct($name, $title=null, $source=array(), $value='', $form=null, $emptyString=null) {
|
public function __construct($name, $title=null, $source=array(), $value='', $form=null, $emptyString=null) {
|
||||||
$this->setSource($source);
|
$this->setSource($source);
|
||||||
|
@ -698,7 +698,7 @@ class Form extends RequestHandler {
|
|||||||
* @return String
|
* @return String
|
||||||
*/
|
*/
|
||||||
public function getAttribute($name) {
|
public function getAttribute($name) {
|
||||||
return @$this->attributes[$name];
|
if(isset($this->attributes[$name])) return $this->attributes[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttributes() {
|
public function getAttributes() {
|
||||||
@ -1093,13 +1093,17 @@ class Form extends RequestHandler {
|
|||||||
return $this->messageType;
|
return $this->messageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
protected function getMessageFromSession() {
|
protected function getMessageFromSession() {
|
||||||
if($this->message || $this->messageType) {
|
if($this->message || $this->messageType) {
|
||||||
return $this->message;
|
return $this->message;
|
||||||
}
|
} else {
|
||||||
|
|
||||||
$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
|
$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
|
||||||
$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
|
$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
|
||||||
|
|
||||||
|
return $this->message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -416,8 +416,7 @@ class FormField extends RequestHandler {
|
|||||||
*/
|
*/
|
||||||
public function getAttribute($name) {
|
public function getAttribute($name) {
|
||||||
$attrs = $this->getAttributes();
|
$attrs = $this->getAttributes();
|
||||||
|
if(isset($attrs[$name])) return $attrs[$name];
|
||||||
return @$attrs[$name];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,10 +62,10 @@ class HtmlEditorSanitiser {
|
|||||||
foreach(explode(',', $validElements) as $validElement) {
|
foreach(explode(',', $validElements) as $validElement) {
|
||||||
if(preg_match($elementRuleRegExp, $validElement, $matches)) {
|
if(preg_match($elementRuleRegExp, $validElement, $matches)) {
|
||||||
|
|
||||||
$prefix = @$matches[1];
|
$prefix = isset($matches[1]) ? $matches[1] : null;
|
||||||
$elementName = @$matches[2];
|
$elementName = isset($matches[2]) ? $matches[2] : null;
|
||||||
$outputName = @$matches[3];
|
$outputName = isset($matches[3]) ? $matches[3] : null;
|
||||||
$attrData = @$matches[4];
|
$attrData = isset($matches[4]) ? $matches[4] : null;
|
||||||
|
|
||||||
// Create the new element
|
// Create the new element
|
||||||
$element = new stdClass();
|
$element = new stdClass();
|
||||||
@ -91,10 +91,10 @@ class HtmlEditorSanitiser {
|
|||||||
if(preg_match($attrRuleRegExp, $attr, $matches)) {
|
if(preg_match($attrRuleRegExp, $attr, $matches)) {
|
||||||
$attr = new stdClass();
|
$attr = new stdClass();
|
||||||
|
|
||||||
$attrType = @$matches[1];
|
$attrType = isset($matches[1]) ? $matches[1] : null;
|
||||||
$attrName = str_replace('::', ':', @$matches[2]);
|
$attrName = isset($matches[2]) ? str_replace('::', ':', $matches[2]) : null;
|
||||||
$prefix = @$matches[3];
|
$prefix = isset($matches[3]) ? $matches[3] : null;
|
||||||
$value = @$matches[4];
|
$value = isset($matches[4]) ? $matches[4] : null;
|
||||||
|
|
||||||
// Required
|
// Required
|
||||||
if($attrType === '!') {
|
if($attrType === '!') {
|
||||||
|
@ -1297,11 +1297,15 @@ class UploadField extends FileField {
|
|||||||
$nameFilter = FileNameFilter::create();
|
$nameFilter = FileNameFilter::create();
|
||||||
$filteredFile = basename($nameFilter->filter($originalFile));
|
$filteredFile = basename($nameFilter->filter($originalFile));
|
||||||
|
|
||||||
|
// Resolve expected folder name
|
||||||
|
$folderName = $this->getFolderName();
|
||||||
|
$folder = Folder::find_or_make($folderName);
|
||||||
|
$parentPath = BASE_PATH."/".$folder->getFilename();
|
||||||
|
|
||||||
// check if either file exists
|
// check if either file exists
|
||||||
$folder = $this->getFolderName();
|
|
||||||
$exists = false;
|
$exists = false;
|
||||||
foreach(array($originalFile, $filteredFile) as $file) {
|
foreach(array($originalFile, $filteredFile) as $file) {
|
||||||
if(file_exists(ASSETS_PATH."/$folder/$file")) {
|
if(file_exists($parentPath.$file)) {
|
||||||
$exists = true;
|
$exists = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -616,7 +616,8 @@ class GridField extends FormField {
|
|||||||
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
|
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
|
||||||
$html = '';
|
$html = '';
|
||||||
$data = $request->requestVars();
|
$data = $request->requestVars();
|
||||||
$fieldData = @$data[$this->getName()];
|
$name = $this->getName();
|
||||||
|
$fieldData = isset($data[$name]) ? $data[$name] : null;
|
||||||
|
|
||||||
// Update state from client
|
// Update state from client
|
||||||
$state = $this->getState(false);
|
$state = $this->getState(false);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
(function($){
|
(function($){
|
||||||
|
|
||||||
var decodePath = function(str) {
|
var decodePath = function(str) {
|
||||||
return str.replace(/%2C/g,',').replace(/\&/g, '&').trim();
|
return str.replace(/%2C/g,',').replace(/\&/g, '&').replace(/^\s+|\s+$/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
$.extend({
|
$.extend({
|
||||||
|
@ -212,13 +212,15 @@ abstract class SS_Database {
|
|||||||
switch($changes['command']) {
|
switch($changes['command']) {
|
||||||
case 'create':
|
case 'create':
|
||||||
$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'], $changes['options'],
|
$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'], $changes['options'],
|
||||||
@$changes['advancedOptions']);
|
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'alter':
|
case 'alter':
|
||||||
$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
|
$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
|
||||||
$changes['alteredFields'], $changes['alteredIndexes'], $changes['alteredOptions'],
|
$changes['alteredFields'], $changes['alteredIndexes'], $changes['alteredOptions'],
|
||||||
@$changes['advancedOptions']);
|
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,9 +163,13 @@ class SS_HTML4Value extends SS_HTMLValue {
|
|||||||
// Reset the document if we're in an invalid state for some reason
|
// Reset the document if we're in an invalid state for some reason
|
||||||
if (!$this->isValid()) $this->setDocument(null);
|
if (!$this->isValid()) $this->setDocument(null);
|
||||||
|
|
||||||
return @$this->getDocument()->loadHTML(
|
$errorState = libxml_use_internal_errors(true);
|
||||||
|
$result = $this->getDocument()->loadHTML(
|
||||||
'<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head>' .
|
'<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head>' .
|
||||||
"<body>$content</body></html>"
|
"<body>$content</body></html>"
|
||||||
);
|
);
|
||||||
|
libxml_clear_errors();
|
||||||
|
libxml_use_internal_errors($errorState);
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,8 @@ class URLSegmentFilter extends Object {
|
|||||||
'/[_.]+/u' => '-', // underscores and dots to dashes
|
'/[_.]+/u' => '-', // underscores and dots to dashes
|
||||||
'/[^A-Za-z0-9\-]+/u' => '', // remove non-ASCII chars, only allow alphanumeric and dashes
|
'/[^A-Za-z0-9\-]+/u' => '', // remove non-ASCII chars, only allow alphanumeric and dashes
|
||||||
'/[\-]{2,}/u' => '-', // remove duplicate dashes
|
'/[\-]{2,}/u' => '-', // remove duplicate dashes
|
||||||
'/^[\-_]/u' => '', // Remove all leading dashes or underscores
|
'/^[\-]+/u' => '', // Remove all leading dashes
|
||||||
|
'/[\-]+$/u' => '' // Remove all trailing dashes
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -229,8 +229,8 @@ class ShortcodeParser {
|
|||||||
'text' => $match[0][0],
|
'text' => $match[0][0],
|
||||||
's' => $match[0][1],
|
's' => $match[0][1],
|
||||||
'e' => $match[0][1] + strlen($match[0][0]),
|
'e' => $match[0][1] + strlen($match[0][0]),
|
||||||
'open' => @$match['open'][0],
|
'open' => isset($match['open'][0]) ? $match['open'][0] : null,
|
||||||
'close' => @$match['close'][0],
|
'close' => isset($match['close'][0]) ? $match['close'][0] : null,
|
||||||
'attrs' => $attrs,
|
'attrs' => $attrs,
|
||||||
'content' => '',
|
'content' => '',
|
||||||
'escaped' => !empty($match['oesc'][0]) || !empty($match['cesc1'][0]) || !empty($match['cesc2'][0])
|
'escaped' => !empty($match['oesc'][0]) || !empty($match['cesc1'][0]) || !empty($match['cesc2'][0])
|
||||||
|
@ -105,6 +105,11 @@ class ChangePasswordForm extends Form {
|
|||||||
// TODO Add confirmation message to login redirect
|
// TODO Add confirmation message to login redirect
|
||||||
Session::clear('AutoLoginHash');
|
Session::clear('AutoLoginHash');
|
||||||
|
|
||||||
|
// Clear locked out status
|
||||||
|
$member->LockedOutUntil = null;
|
||||||
|
$member->FailedLoginCount = null;
|
||||||
|
$member->write();
|
||||||
|
|
||||||
if (isset($_REQUEST['BackURL'])
|
if (isset($_REQUEST['BackURL'])
|
||||||
&& $_REQUEST['BackURL']
|
&& $_REQUEST['BackURL']
|
||||||
// absolute redirection URLs may cause spoofing
|
// absolute redirection URLs may cause spoofing
|
||||||
|
@ -1180,10 +1180,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
|||||||
* editing this member.
|
* editing this member.
|
||||||
*/
|
*/
|
||||||
public function getCMSFields() {
|
public function getCMSFields() {
|
||||||
require_once('Zend/Date.php');
|
require_once 'Zend/Date.php';
|
||||||
|
|
||||||
$fields = parent::getCMSFields();
|
|
||||||
|
|
||||||
|
$self = $this;
|
||||||
|
$this->beforeUpdateCMSFields(function($fields) use ($self) {
|
||||||
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
|
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
|
||||||
|
|
||||||
$password = new ConfirmedPasswordField(
|
$password = new ConfirmedPasswordField(
|
||||||
@ -1194,7 +1194,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
|||||||
true // showOnClick
|
true // showOnClick
|
||||||
);
|
);
|
||||||
$password->setCanBeEmpty(true);
|
$password->setCanBeEmpty(true);
|
||||||
if(!$this->ID) $password->showOnClick = false;
|
if( ! $self->ID) $password->showOnClick = false;
|
||||||
$mainFields->replaceField('Password', $password);
|
$mainFields->replaceField('Password', $password);
|
||||||
|
|
||||||
$mainFields->replaceField('Locale', new DropdownField(
|
$mainFields->replaceField('Locale', new DropdownField(
|
||||||
@ -1210,11 +1210,14 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
|||||||
$mainFields->removeByName('PasswordExpiry');
|
$mainFields->removeByName('PasswordExpiry');
|
||||||
$mainFields->removeByName('LockedOutUntil');
|
$mainFields->removeByName('LockedOutUntil');
|
||||||
|
|
||||||
if(!self::config()->lock_out_after_incorrect_logins) {
|
if( ! $self->config()->lock_out_after_incorrect_logins) {
|
||||||
$mainFields->removeByName('FailedLoginCount');
|
$mainFields->removeByName('FailedLoginCount');
|
||||||
}
|
}
|
||||||
|
|
||||||
$mainFields->removeByName('Salt');
|
$mainFields->removeByName('Salt');
|
||||||
|
$mainFields->removeByName('NumVisit');
|
||||||
|
|
||||||
|
$mainFields->makeFieldReadonly('LastVisited');
|
||||||
|
|
||||||
$fields->removeByName('Subscriptions');
|
$fields->removeByName('Subscriptions');
|
||||||
|
|
||||||
@ -1239,17 +1242,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Add permission field (readonly to avoid complicated group assignment logic).
|
// Add permission field (readonly to avoid complicated group assignment logic).
|
||||||
// This should only be available for existing records, as new records start
|
// This should only be available for existing records, as new records start
|
||||||
// with no permissions until they have a group assignment anyway.
|
// with no permissions until they have a group assignment anyway.
|
||||||
if($this->ID) {
|
if($self->ID) {
|
||||||
$permissionsField = new PermissionCheckboxSetField_Readonly(
|
$permissionsField = new PermissionCheckboxSetField_Readonly(
|
||||||
'Permissions',
|
'Permissions',
|
||||||
false,
|
false,
|
||||||
'Permission',
|
'Permission',
|
||||||
'GroupID',
|
'GroupID',
|
||||||
// we don't want parent relationships, they're automatically resolved in the field
|
// we don't want parent relationships, they're automatically resolved in the field
|
||||||
$this->getManyManyComponents('Groups')
|
$self->getManyManyComponents('Groups')
|
||||||
);
|
);
|
||||||
$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
|
$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
|
||||||
$fields->addFieldToTab('Root.Permissions', $permissionsField);
|
$fields->addFieldToTab('Root.Permissions', $permissionsField);
|
||||||
@ -1259,44 +1263,42 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
|||||||
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
|
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
|
||||||
if($permissionsTab) $permissionsTab->addExtraClass('readonly');
|
if($permissionsTab) $permissionsTab->addExtraClass('readonly');
|
||||||
|
|
||||||
$localDateFormat = Zend_Locale_Data::getContent(new Zend_Locale($this->Locale), 'date', 'short');
|
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
|
||||||
// Ensure short dates always use four digit dates to avoid confusion
|
|
||||||
$localDateFormat = preg_replace('/(^|[^y])yy($|[^y])/', '$1yyyy$2', $localDateFormat);
|
|
||||||
$dateFormatMap = array(
|
$dateFormatMap = array(
|
||||||
$this->DateFormat => Zend_Date::now()->toString($this->DateFormat),
|
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
|
||||||
$localDateFormat => Zend_Date::now()->toString($localDateFormat),
|
|
||||||
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
|
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
|
||||||
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
|
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
|
||||||
'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
|
'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
|
||||||
);
|
);
|
||||||
|
$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
|
||||||
|
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
|
||||||
$mainFields->push(
|
$mainFields->push(
|
||||||
$dateFormatField = new MemberDatetimeOptionsetField(
|
$dateFormatField = new MemberDatetimeOptionsetField(
|
||||||
'DateFormat',
|
'DateFormat',
|
||||||
$this->fieldLabel('DateFormat'),
|
$self->fieldLabel('DateFormat'),
|
||||||
$dateFormatMap
|
$dateFormatMap
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$dateFormatField->setValue($this->DateFormat);
|
$dateFormatField->setValue($self->DateFormat);
|
||||||
|
|
||||||
$localTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($this->Locale));
|
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
|
||||||
$timeFormatMap = array(
|
$timeFormatMap = array(
|
||||||
$this->TimeFormat => Zend_Date::now()->toString($this->TimeFormat),
|
|
||||||
$localTimeFormat => Zend_Date::now()->toString($localTimeFormat),
|
|
||||||
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
|
'h:mm a' => Zend_Date::now()->toString('h:mm a'),
|
||||||
'H:mm' => Zend_Date::now()->toString('H:mm'),
|
'H:mm' => Zend_Date::now()->toString('H:mm'),
|
||||||
);
|
);
|
||||||
|
$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
|
||||||
|
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
|
||||||
$mainFields->push(
|
$mainFields->push(
|
||||||
$timeFormatField = new MemberDatetimeOptionsetField(
|
$timeFormatField = new MemberDatetimeOptionsetField(
|
||||||
'TimeFormat',
|
'TimeFormat',
|
||||||
$this->fieldLabel('TimeFormat'),
|
$self->fieldLabel('TimeFormat'),
|
||||||
$timeFormatMap
|
$timeFormatMap
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$timeFormatField->setValue($this->TimeFormat);
|
$timeFormatField->setValue($self->TimeFormat);
|
||||||
|
});
|
||||||
|
|
||||||
$this->extend('updateCMSFields', $fields);
|
return parent::getCMSFields();
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Log-in form for the "member" authentication method
|
* Log-in form for the "member" authentication method.
|
||||||
|
*
|
||||||
|
* Available extension points:
|
||||||
|
* - "authenticationFailed": Called when login was not successful.
|
||||||
|
* Arguments: $data containing the form submission
|
||||||
|
* - "forgotPassword": Called before forgot password logic kicks in,
|
||||||
|
* allowing extensions to "veto" execution by returning FALSE.
|
||||||
|
* Arguments: $member containing the detected Member record
|
||||||
|
*
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage security
|
* @subpackage security
|
||||||
*/
|
*/
|
||||||
@ -256,9 +264,12 @@ JS
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forgot password form handler method
|
* Forgot password form handler method.
|
||||||
*
|
* Called when the user clicks on "I've lost my password".
|
||||||
* This method is called when the user clicks on "I've lost my password"
|
* Extensions can use the 'forgotPassword' method to veto executing
|
||||||
|
* the logic, by returning FALSE. In this case, the user will be redirected back
|
||||||
|
* to the form without further action. It is recommended to set a message
|
||||||
|
* in the form detailing why the action was denied.
|
||||||
*
|
*
|
||||||
* @param array $data Submitted data
|
* @param array $data Submitted data
|
||||||
*/
|
*/
|
||||||
@ -267,6 +278,12 @@ JS
|
|||||||
$SQL_email = $SQL_data['Email'];
|
$SQL_email = $SQL_data['Email'];
|
||||||
$member = DataObject::get_one('Member', "\"Email\" = '{$SQL_email}'");
|
$member = DataObject::get_one('Member', "\"Email\" = '{$SQL_email}'");
|
||||||
|
|
||||||
|
// Allow vetoing forgot password requests
|
||||||
|
$results = $this->extend('forgotPassword', $member);
|
||||||
|
if($results && is_array($results) && in_array(false, $results, true)) {
|
||||||
|
return $this->controller->redirect('Security/lostpassword');
|
||||||
|
}
|
||||||
|
|
||||||
if($member) {
|
if($member) {
|
||||||
$token = $member->generateAutologinTokenAndStoreHash();
|
$token = $member->generateAutologinTokenAndStoreHash();
|
||||||
|
|
||||||
|
@ -184,14 +184,14 @@ class CmsUiContext extends BehatContext {
|
|||||||
* @When /^I expand the "([^"]*)" CMS Panel$/
|
* @When /^I expand the "([^"]*)" CMS Panel$/
|
||||||
*/
|
*/
|
||||||
public function iExpandTheCmsPanel() {
|
public function iExpandTheCmsPanel() {
|
||||||
// TODO Make dynamic, currently hardcoded to first panel
|
//Tries to find the first visiable toggle in the page
|
||||||
$page = $this->getSession()->getPage();
|
$page = $this->getSession()->getPage();
|
||||||
|
$toggle_elements = $page->findAll('css', '.toggle-expand');
|
||||||
$panel_toggle_element = $page->find('css', '.cms-content > .cms-panel > .cms-panel-toggle > .toggle-expand');
|
assertNotNull($toggle_elements, 'Panel toggle not found');
|
||||||
assertNotNull($panel_toggle_element, 'Panel toggle not found');
|
foreach($toggle_elements as $toggle){
|
||||||
|
if($toggle->isVisible()){
|
||||||
if ($panel_toggle_element->isVisible()) {
|
$toggle->click();
|
||||||
$panel_toggle_element->click();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
tests/behat/features/files/document.pdf
Normal file
BIN
tests/behat/features/files/document.pdf
Normal file
Binary file not shown.
@ -12,6 +12,58 @@ class FixtureBlueprintTest extends SapphireTest {
|
|||||||
'FixtureFactoryTest_DataObjectRelation'
|
'FixtureFactoryTest_DataObjectRelation'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public function testCreateWithRelationshipExtraFields() {
|
||||||
|
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
||||||
|
|
||||||
|
$relation1 = new FixtureFactoryTest_DataObjectRelation();
|
||||||
|
$relation1->write();
|
||||||
|
$relation2 = new FixtureFactoryTest_DataObjectRelation();
|
||||||
|
$relation2->write();
|
||||||
|
|
||||||
|
// in YAML these look like
|
||||||
|
// RelationName:
|
||||||
|
// - =>Relational.obj:
|
||||||
|
// ExtraFieldName: test
|
||||||
|
// - =>..
|
||||||
|
$obj = $blueprint->createObject(
|
||||||
|
'one',
|
||||||
|
array(
|
||||||
|
'ManyMany' =>
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
"=>FixtureFactoryTest_DataObjectRelation.relation1" => array(),
|
||||||
|
"Label" => 'This is a label for relation 1'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
"=>FixtureFactoryTest_DataObjectRelation.relation2" => array(),
|
||||||
|
"Label" => 'This is a label for relation 2'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'FixtureFactoryTest_DataObjectRelation' => array(
|
||||||
|
'relation1' => $relation1->ID,
|
||||||
|
'relation2' => $relation2->ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(2, $obj->ManyMany()->Count());
|
||||||
|
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
|
||||||
|
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Label' => 'This is a label for relation 1'),
|
||||||
|
$obj->ManyMany()->getExtraData('ManyMany', $relation1->ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array('Label' => 'This is a label for relation 2'),
|
||||||
|
$obj->ManyMany()->getExtraData('ManyMany', $relation2->ID)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testCreateWithoutData() {
|
public function testCreateWithoutData() {
|
||||||
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
||||||
$obj = $blueprint->createObject('one');
|
$obj = $blueprint->createObject('one');
|
||||||
@ -28,6 +80,7 @@ class FixtureBlueprintTest extends SapphireTest {
|
|||||||
$this->assertEquals('My Name', $obj->Name);
|
$this->assertEquals('My Name', $obj->Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testCreateWithRelationship() {
|
public function testCreateWithRelationship() {
|
||||||
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
||||||
|
|
||||||
@ -127,7 +180,7 @@ class FixtureBlueprintTest extends SapphireTest {
|
|||||||
$this->assertEquals(99, $obj->ID);
|
$this->assertEquals(99, $obj->ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCallbackOnBeforeCreate() {
|
public function testCallbackOnBeforeCreate() {
|
||||||
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
||||||
$this->_called = 0;
|
$this->_called = 0;
|
||||||
$self = $this;
|
$self = $this;
|
||||||
@ -144,7 +197,7 @@ class FixtureBlueprintTest extends SapphireTest {
|
|||||||
$this->_called = 0;
|
$this->_called = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCallbackOnAfterCreate() {
|
public function testCallbackOnAfterCreate() {
|
||||||
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
|
||||||
$this->_called = 0;
|
$this->_called = 0;
|
||||||
$self = $this;
|
$self = $this;
|
||||||
@ -161,7 +214,7 @@ class FixtureBlueprintTest extends SapphireTest {
|
|||||||
$this->_called = 0;
|
$this->_called = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDefineWithDefaultCustomSetters() {
|
public function testDefineWithDefaultCustomSetters() {
|
||||||
$blueprint = new FixtureBlueprint(
|
$blueprint = new FixtureBlueprint(
|
||||||
'FixtureFactoryTest_DataObject',
|
'FixtureFactoryTest_DataObject',
|
||||||
null,
|
null,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage tests
|
* @subpackage tests
|
||||||
@ -151,19 +152,37 @@ class FixtureFactoryTest extends SapphireTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
|
class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
|
||||||
|
|
||||||
private static $db = array(
|
private static $db = array(
|
||||||
"Name" => "Varchar"
|
"Name" => "Varchar"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
"ManyMany" => "FixtureFactoryTest_DataObjectRelation"
|
"ManyMany" => "FixtureFactoryTest_DataObjectRelation"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static $many_many_extraFields = array(
|
||||||
|
"ManyMany" => array(
|
||||||
|
"Label" => "Varchar"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly {
|
class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly {
|
||||||
|
|
||||||
private static $db = array(
|
private static $db = array(
|
||||||
"Name" => "Varchar"
|
"Name" => "Varchar"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $belongs_many_many = array(
|
private static $belongs_many_many = array(
|
||||||
"TestParent" => "FixtureFactoryTest_DataObject"
|
"TestParent" => "FixtureFactoryTest_DataObject"
|
||||||
);
|
);
|
||||||
|
@ -338,4 +338,21 @@ class FolderTest extends SapphireTest {
|
|||||||
))->count());
|
))->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIllegalFilenames() {
|
||||||
|
|
||||||
|
// Test that generating a filename with invalid characters generates a correctly named folder.
|
||||||
|
$folder = Folder::find_or_make('/FolderTest/EN_US Lang');
|
||||||
|
$this->assertEquals(ASSETS_DIR.'/FolderTest/EN-US-Lang/', $folder->getRelativePath());
|
||||||
|
|
||||||
|
// Test repeatitions of folder
|
||||||
|
$folder2 = Folder::find_or_make('/FolderTest/EN_US Lang');
|
||||||
|
$this->assertEquals($folder->ID, $folder2->ID);
|
||||||
|
|
||||||
|
$folder3 = Folder::find_or_make('/FolderTest/EN--US_L!ang');
|
||||||
|
$this->assertEquals($folder->ID, $folder3->ID);
|
||||||
|
|
||||||
|
$folder4 = Folder::find_or_make('/FolderTest/EN-US-Lang');
|
||||||
|
$this->assertEquals($folder->ID, $folder4->ID);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -378,6 +378,93 @@ class UploadTest extends SapphireTest {
|
|||||||
$file2->delete();
|
$file2->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReplaceFileWithLoadIntoFile() {
|
||||||
|
// create tmp file
|
||||||
|
$tmpFileName = 'UploadTest-testUpload.txt';
|
||||||
|
$tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
|
||||||
|
$tmpFileContent = '';
|
||||||
|
for ($i = 0; $i < 10000; $i++)
|
||||||
|
$tmpFileContent .= '0';
|
||||||
|
file_put_contents($tmpFilePath, $tmpFileContent);
|
||||||
|
|
||||||
|
// emulates the $_FILES array
|
||||||
|
$tmpFile = array(
|
||||||
|
'name' => $tmpFileName,
|
||||||
|
'type' => 'text/plaintext',
|
||||||
|
'size' => filesize($tmpFilePath),
|
||||||
|
'tmp_name' => $tmpFilePath,
|
||||||
|
'extension' => 'txt',
|
||||||
|
'error' => UPLOAD_ERR_OK,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure there are none here, otherwise they get renamed incorrectly for the test.
|
||||||
|
$this->deleteTestUploadFiles("/UploadTest-testUpload.*/");
|
||||||
|
|
||||||
|
$v = new UploadTest_Validator();
|
||||||
|
|
||||||
|
// test upload into default folder
|
||||||
|
$u = new Upload();
|
||||||
|
$u->setValidator($v);
|
||||||
|
$u->load($tmpFile);
|
||||||
|
$file = $u->getFile();
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'UploadTest-testUpload.txt',
|
||||||
|
$file->Name,
|
||||||
|
'File is uploaded without extension'
|
||||||
|
);
|
||||||
|
$this->assertFileExists(
|
||||||
|
BASE_PATH . '/' . $file->getFilename(),
|
||||||
|
'File exists'
|
||||||
|
);
|
||||||
|
|
||||||
|
// replace=true
|
||||||
|
$u = new Upload();
|
||||||
|
$u->setValidator($v);
|
||||||
|
$u->setReplaceFile(true);
|
||||||
|
$u->loadIntoFile($tmpFile, new File());
|
||||||
|
$file2 = $u->getFile();
|
||||||
|
$this->assertEquals(
|
||||||
|
'UploadTest-testUpload.txt',
|
||||||
|
$file2->Name,
|
||||||
|
'File does not receive new name'
|
||||||
|
);
|
||||||
|
$this->assertFileExists(
|
||||||
|
BASE_PATH . '/' . $file2->getFilename(),
|
||||||
|
'File exists'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
$file->ID,
|
||||||
|
$file2->ID,
|
||||||
|
'File database record is the same'
|
||||||
|
);
|
||||||
|
|
||||||
|
// replace=false
|
||||||
|
$u = new Upload();
|
||||||
|
$u->setValidator($v);
|
||||||
|
$u->setReplaceFile(false);
|
||||||
|
$u->loadIntoFile($tmpFile, new File());
|
||||||
|
$file3 = $u->getFile();
|
||||||
|
$this->assertEquals(
|
||||||
|
'UploadTest-testUpload2.txt',
|
||||||
|
$file3->Name,
|
||||||
|
'File does receive new name'
|
||||||
|
);
|
||||||
|
$this->assertFileExists(
|
||||||
|
BASE_PATH . '/' . $file3->getFilename(),
|
||||||
|
'File exists'
|
||||||
|
);
|
||||||
|
$this->assertGreaterThan(
|
||||||
|
$file2->ID,
|
||||||
|
$file3->ID,
|
||||||
|
'File database record is not the same'
|
||||||
|
);
|
||||||
|
|
||||||
|
$file->delete();
|
||||||
|
$file2->delete();
|
||||||
|
$file3->delete();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
class UploadTest_Validator extends Upload_Validator implements TestOnly {
|
class UploadTest_Validator extends Upload_Validator implements TestOnly {
|
||||||
|
|
||||||
|
@ -342,7 +342,6 @@ class SQLQueryTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testSetWhereAny() {
|
public function testSetWhereAny() {
|
||||||
$query = new SQLQuery();
|
$query = new SQLQuery();
|
||||||
$query->setFrom('MyTable');
|
$query->setFrom('MyTable');
|
||||||
@ -352,7 +351,6 @@ class SQLQueryTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testSelectFirst() {
|
public function testSelectFirst() {
|
||||||
|
|
||||||
// Test first from sequence
|
// Test first from sequence
|
||||||
$query = new SQLQuery();
|
$query = new SQLQuery();
|
||||||
$query->setFrom('"SQLQueryTest_DO"');
|
$query->setFrom('"SQLQueryTest_DO"');
|
||||||
@ -398,7 +396,6 @@ class SQLQueryTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testSelectLast() {
|
public function testSelectLast() {
|
||||||
|
|
||||||
// Test last in sequence
|
// Test last in sequence
|
||||||
$query = new SQLQuery();
|
$query = new SQLQuery();
|
||||||
$query->setFrom('"SQLQueryTest_DO"');
|
$query->setFrom('"SQLQueryTest_DO"');
|
||||||
|
@ -41,8 +41,8 @@ class URLSegmentFilterTest extends SapphireTest {
|
|||||||
public function testReplacesCommonNonAsciiCharacters() {
|
public function testReplacesCommonNonAsciiCharacters() {
|
||||||
$f = new URLSegmentFilter();
|
$f = new URLSegmentFilter();
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
urlencode('aa1-'),
|
urlencode('aa1-a'),
|
||||||
$f->filter('Aa1~!@#$%^*()_`-=;\':"[]\{}|,./<>?')
|
$f->filter('Aa1~!@#$%^*()_`-=;\':"[]\{}|,./<>?a')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,4 +77,14 @@ class URLSegmentFilterTest extends SapphireTest {
|
|||||||
$this->assertEquals('url-contains-dot', $filter->filter('url-contains.dot'));
|
$this->assertEquals('url-contains-dot', $filter->filter('url-contains.dot'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRemoveLeadingDashes() {
|
||||||
|
$filter = new URLSegmentFilter();
|
||||||
|
$this->assertEquals('url-has-leading-dashes', $filter->filter('---url-has-leading-dashes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReplacesTrailingDashes() {
|
||||||
|
$filter = new URLSegmentFilter();
|
||||||
|
$this->assertEquals('url-has-trailing-dashes', $filter->filter('url-has-trailing-dashes--'));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ class BasicAuthTest extends FunctionalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testBasicAuthEnabledWithoutLogin() {
|
public function testBasicAuthEnabledWithoutLogin() {
|
||||||
$origUser = @$_SERVER['PHP_AUTH_USER'];
|
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
|
||||||
$origPw = @$_SERVER['PHP_AUTH_PW'];
|
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
@ -40,8 +40,8 @@ class BasicAuthTest extends FunctionalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testBasicAuthDoesntCallActionOrFurtherInitOnAuthFailure() {
|
public function testBasicAuthDoesntCallActionOrFurtherInitOnAuthFailure() {
|
||||||
$origUser = @$_SERVER['PHP_AUTH_USER'];
|
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
|
||||||
$origPw = @$_SERVER['PHP_AUTH_PW'];
|
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
@ -60,8 +60,8 @@ class BasicAuthTest extends FunctionalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testBasicAuthEnabledWithPermission() {
|
public function testBasicAuthEnabledWithPermission() {
|
||||||
$origUser = @$_SERVER['PHP_AUTH_USER'];
|
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
|
||||||
$origPw = @$_SERVER['PHP_AUTH_PW'];
|
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
|
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
|
||||||
@ -83,8 +83,8 @@ class BasicAuthTest extends FunctionalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testBasicAuthEnabledWithoutPermission() {
|
public function testBasicAuthEnabledWithoutPermission() {
|
||||||
$origUser = @$_SERVER['PHP_AUTH_USER'];
|
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
|
||||||
$origPw = @$_SERVER['PHP_AUTH_PW'];
|
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
|
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
|
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
|
||||||
|
@ -610,6 +610,22 @@ class MemberTest extends FunctionalTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that extensions using updateCMSFields() are applied correctly
|
||||||
|
*/
|
||||||
|
public function testUpdateCMSFields() {
|
||||||
|
Member::add_extension('MemberTest_FieldsExtension');
|
||||||
|
|
||||||
|
$member = singleton('Member');
|
||||||
|
$fields = $member->getCMSFields();
|
||||||
|
|
||||||
|
$this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
|
||||||
|
$this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
|
||||||
|
$this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
|
||||||
|
|
||||||
|
Member::remove_extension('MemberTest_FieldsExtension');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that all members are returned
|
* Test that all members are returned
|
||||||
*/
|
*/
|
||||||
@ -847,6 +863,18 @@ class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
|
class MemberTest_FieldsExtension extends DataExtension implements TestOnly {
|
||||||
|
|
||||||
|
public function updateCMSFields(FieldList $fields) {
|
||||||
|
$fields->addFieldToTab('Root.Main', new TextField('TestMemberField', 'Test'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage tests
|
* @subpackage tests
|
||||||
|
@ -213,6 +213,9 @@ class SecurityTest extends FunctionalTest {
|
|||||||
|
|
||||||
public function testChangePasswordFromLostPassword() {
|
public function testChangePasswordFromLostPassword() {
|
||||||
$admin = $this->objFromFixture('Member', 'test');
|
$admin = $this->objFromFixture('Member', 'test');
|
||||||
|
$admin->FailedLoginCount = 99;
|
||||||
|
$admin->LockedOutUntil = SS_Datetime::now()->Format('Y-m-d H:i:s');
|
||||||
|
$admin->write();
|
||||||
|
|
||||||
$this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password');
|
$this->assertNull($admin->AutoLoginHash, 'Hash is empty before lost password');
|
||||||
|
|
||||||
@ -243,6 +246,10 @@ class SecurityTest extends FunctionalTest {
|
|||||||
$goodResponse = $this->doTestLoginForm('sam@silverstripe.com' , 'changedPassword');
|
$goodResponse = $this->doTestLoginForm('sam@silverstripe.com' , 'changedPassword');
|
||||||
$this->assertEquals(302, $goodResponse->getStatusCode());
|
$this->assertEquals(302, $goodResponse->getStatusCode());
|
||||||
$this->assertEquals($this->idFromFixture('Member', 'test'), $this->session()->inst_get('loggedInAs'));
|
$this->assertEquals($this->idFromFixture('Member', 'test'), $this->session()->inst_get('loggedInAs'));
|
||||||
|
|
||||||
|
$admin = DataObject::get_by_id('Member', $admin->ID, false);
|
||||||
|
$this->assertNull($admin->LockedOutUntil);
|
||||||
|
$this->assertEquals(0, $admin->FailedLoginCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRepeatedLoginAttemptsLockingPeopleOut() {
|
public function testRepeatedLoginAttemptsLockingPeopleOut() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user