Merge branch '3.1'

Conflicts:
	forms/Form.php
	forms/FormField.php
	security/Member.php
	security/MemberLoginForm.php
This commit is contained in:
Simon Welsh 2014-03-10 22:58:49 +13:00
commit d431e98ecf
54 changed files with 1047 additions and 680 deletions

View File

@ -245,7 +245,7 @@ class Director implements TemplateGlobalProvider {
Requirements::set_backend(new Requirements_Backend());
// Handle absolute URLs
if (@parse_url($url, PHP_URL_HOST) != '') {
if (parse_url($url, PHP_URL_HOST)) {
$bits = parse_url($url);
// If a port is mentioned in the absolute URL, be sure to add that into the
// HTTP host

View File

@ -401,7 +401,12 @@ class Debug {
$reporter = self::create_debug_view();
// 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;
$reporter->writeHeader($httpRequest);

View File

@ -128,8 +128,28 @@ class FixtureBlueprint {
// Populate all relations
if($data) foreach($data as $fieldName => $fieldVal) {
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
$obj->write();
$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));
foreach($items as $item) {
// Check for correct format: =><relationname>.<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);
}
$obj->write();
if($obj->has_many($fieldName)) {
$obj->getComponents($fieldName)->setByIDList($parsedItems);
} elseif($obj->many_many($fieldName)) {
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
}
}
} elseif($obj->has_one($fieldName)) {
// Sets has_one with relation name
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);

View File

@ -167,8 +167,8 @@ class SS_Log {
$message = array(
'errno' => '',
'errstr' => $message,
'errfile' => @$lastTrace['file'],
'errline' => @$lastTrace['line'],
'errfile' => isset($lastTrace['file']) ? $lastTrace['file'] : null,
'errline' => isset($lastTrace['line']) ? $lastTrace['line'] : null,
'errcontext' => $trace
);
}

View File

@ -23,6 +23,9 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
$errorType = 'Notice';
$colour = 'grey';
break;
default:
$errorType = $event['priorityName'];
$colour = 'grey';
}
if(!is_array($event['message'])) {
@ -63,8 +66,8 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
$relfile = Director::makeRelative($errfile);
if($relfile && $relfile[0] == '/') $relfile = substr($relfile, 1);
$host = @$_SERVER['HTTP_HOST'];
$uri = @$_SERVER['REQUEST_URI'];
$host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
$subject = "[$errorType] in $relfile:{$errline} (http://{$host}{$uri})";

View File

@ -26,6 +26,8 @@ class SS_LogErrorFileFormatter implements Zend_Log_Formatter_Interface {
case 'NOTICE':
$errtype = 'Notice';
break;
default:
$errtype = $event['priorityName'];
}
$urlSuffix = '';

View File

@ -408,8 +408,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
/**
* Get an object from the fixture.
*
* @param $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 $className The data class, as specified in your fixture file. Parent classes won't work
* @param string $identifier The identifier string, as provided in your fixture file
*
* @return DataObject
*/
protected function objFromFixture($className, $identifier) {
$obj = $this->getFixtureFactory()->get($className, $identifier);

View File

@ -395,11 +395,15 @@ class InstallRequirements {
*/
function findWebserver() {
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
$webserver = @$_SERVER['SERVER_SIGNATURE'];
if(!$webserver) $webserver = @$_SERVER['SERVER_SOFTWARE'];
if(!empty($_SERVER['SERVER_SIGNATURE'])) {
$webserver = $_SERVER['SERVER_SIGNATURE'];
} elseif(!empty($_SERVER['SERVER_SOFTWARE'])) {
$webserver = $_SERVER['SERVER_SOFTWARE'];
} else {
return false;
}
if($webserver) return strip_tags(trim($webserver));
else return false;
return strip_tags(trim($webserver));
}
/**
@ -1125,7 +1129,7 @@ class InstallRequirements {
$this->testing($testDetails);
return true;
} else {
if(!@$result['cannotCreate']) {
if(empty($result['cannotCreate'])) {
$testDetails[2] .= ". Please create the database manually.";
} else {
$testDetails[2] .= " (user '$databaseConfig[username]' doesn't have CREATE DATABASE permissions.)";
@ -1217,7 +1221,7 @@ class InstallRequirements {
$section = $testDetails[0];
$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;
}
@ -1225,7 +1229,7 @@ class InstallRequirements {
$section = $testDetails[0];
$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;
}

View File

@ -2,34 +2,11 @@
## 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
(`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.
## Changelog
In order to provide more secure default behaviour, we have changed
`choose_site_stage()` to be called on all requests, defaulting to the "Live" stage.
If your logic relies on querying draft content, use `Versioned::reading_stage('Stage')`.
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)
* [framework](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)
* [cms](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)
* [installer](https://github.com/silverstripe/silverstripe-framework/releases/tag/3.0.9)

View File

@ -2,6 +2,9 @@
## 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
* Option for `force_js_to_bottom` on `Requirements` class (ignoring inline `<script>` tags)
* Added `ListDecorator->filterByCallback()` for more sophisticated filtering
@ -13,4 +16,14 @@
## 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
* [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)

View 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)

View File

@ -2,12 +2,28 @@
## 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
### 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
on write to only allow `meta` and `link` elements. The first time after upgrading that you save a page
that has other elements in ExtraMeta they will be deleted.
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
* [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)

View 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)

View File

@ -71,7 +71,7 @@ Example `mysite/_config.php`:
// Customized configuration for running with different database settings.
// Ensure this code comes after ConfigureFromEnv.php
if(Director::isDev()) {
if($db = @$_GET['db']) {
if(isset($_GET['db']) && ($db = $_GET['db'])) {
global $databaseConfig;
if($db == 'sqlite3') $databaseConfig['type'] = 'SQLite3Database';
}

View File

@ -41,7 +41,7 @@ Composer updates regularly, so you should run this command fairly often. These i
## 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
@ -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.
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
a version to download that version explicitly, i.e. this will download the older `3.0.3` release:
You can also specify 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
@ -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:
- 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
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
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`.

View File

@ -1,6 +1,6 @@
# 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).
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 {
listen 80;
server_name example.com;
root /path/to/ss/folder;
root /var/www/example.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`:
server_name site.com www.site.com;
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
# index.php and install.php files) after you installed SilverStripe
# (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;
}
error_page 404 /assets/error-404.html;
error_page 500 /assets/error-500.html;
# 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/ {
sendfile on;
try_files $uri =404;
}
# Deny access to silverstripe-cache, vendor or composer.json/.lock
location ^~ /silverstripe-cache/ {
location ~ /framework/.*(main|rpc|tiny_mce_gzip)\.php$ {
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;
}
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/ {
deny all;
}
location ~ /composer\.(json|lock) {
location ~* /silverstripe-cache/ {
deny all;
}
# Don't serve up any "hidden" files or directories
# (starting with dot, like .htaccess or .git)
# also don't serve web.config files
location ~ /(\.|web\.config) {
location ~* composer\.(json|lock)$ {
deny all;
}
# Block access to yaml files (and don't forget about backup
# files that editors tend to leave behind)
location ~ \.(yml|bak|swp)$ {
deny all;
}
location ~ ~$ {
location ~* /(cms|framework)/silverstripe_version$ {
deny all;
}
# generally don't serve any php-like files
# (as they exist, they would be served as regular files, and not interpreted.
# But as those can contain configuration data, this is bad nevertheless)
# If needed, you can always whitelist entries.
location ~ \.(php|php[345]|phtml|inc)$ {
deny all;
location ~ \.php$ {
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 ~ ^/(cms|framework)/silverstripe_version$ {
deny all;
}
Here is the optional include file `ssl`:
listen 443 ssl;
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.
The above configuration sets up a virtual host `site.com` with
rewrite rules suited for SilverStripe. The location block for php files
passes all php scripts to the FastCGI-wrapper via a TCP socket.
Now you can proceed with the SilverStripe installation normally.

View File

@ -208,7 +208,7 @@ Non-textual elements (such as images and their manipulations) can also be used i
'HeroImage' => 'Image'
);
private static $summary_fields = array(
'Name' => 'Name,
'Name' => 'Name',
'HeroImage.CMSThumbnail' => 'Hero Image'
);
}

View File

@ -66,7 +66,6 @@ Note how the configuration happens in different entwine namespaces
}
});
});
});
}(jQuery));
Load the file in the CMS via setting adding 'mysite/javascript/MyLeftAndMain.Preview.js'

View File

@ -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.
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.
## Built-in Shortcodes
SilverStripe comes with several shortcode parsers already.

View File

@ -21,7 +21,7 @@ In your controller (e.g. `mysite/code/Page.php`):
// either specify the css file manually
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
Requirements::themedCSS('print', 'print');
Requirements::themedCSS('print', null,'print');
}
}

View File

@ -720,7 +720,7 @@ an object, not for displaying the objects contained in the relation.
## Validation and Constraints
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
record is modified outside of the controller and form context.

View File

@ -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()]

View File

@ -1,6 +1,6 @@
# 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
form.
@ -9,14 +9,14 @@ form.
A fully implemented form in SilverStripe includes a couple of classes that
individually have separate concerns.
* Controller - Takes care of assembling the form and receiving data from it.
* Form - Holds sets of fields, actions and validators.
* FormField - Fields that receive data or displays them, e.g input fields.
* FormActions - Often submit buttons that executes actions.
* Validators - Validate the whole form, see [Form validation](form-validation.md) topic for more information.
* Controller—Takes care of assembling the form and receiving data from it.
* Form—Holds sets of fields, actions and validators.
* FormField —Fields that receive data or displays them, e.g input fields.
* FormActions—Often submit buttons that executes actions.
* Validators—Validate the whole form.
Depending on your needs you can customize and override any of the above classes,
however the defaults are often sufficient.
Depending on your needs you can customize and override any of the above classes;
the defaults, however, are often sufficient.
## The Controller
@ -53,12 +53,13 @@ in a controller.
The name of the form ("HelloForm") is passed into the `Form` constructor as a
second argument. It needs to match the method name.
Since 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
it to the `$allowed_actions` array.
Because forms need a URL, the `HelloForm()` method needs to be handled like any
other controller action. To grant it access through URLs, we add it to the
`$allowed_actions` array.
Form actions ("doSayHello") on the other hand should NOT be included here, these
are handled separately through `Form->httpSubmission()`.
Form actions ("doSayHello"), on the other hand, should _not_ be included in
`$allowed_actions`; these are handled separately through
`Form->httpSubmission()`.
You can control access on form actions either by conditionally removing a
`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**
:::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 class="warning" markdown='1'>
Be sure to add the Form name 'HelloForm' to the Controller::$allowed_actions()
to be sure that form submissions get through to the correct action.
Be sure to add the Form name 'HelloForm' to your controller's $allowed_actions
array to enable form submissions.
</div>
<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.
These are functionally equivalent, but allows PHP to chain operations like `setTitle()` without assigning the field
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().
You'll notice that we've used a new notation for creating form fields, using
`create()` instead of the `new` operator. These are functionally equivalent, but
allows PHP to chain operations like `setTitle()` without assigning the field
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>
## 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
arguments:
* `$controller`: This must be an instance of the controller that contains the form, often `$this`.
* `$name`: This must be the name of the method on that controller that is called to return the form. The first two
fields allow the form object to be re-created after submission. **It's vital that they are properly set - if you ever
have problems with 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.
* `$controller`: This must be an instance of the controller that contains the
form, often `$this`.
* `$name`: This must be the name of the method on that controller that is
called to return the form. The first two arguments allow the form object
to be re-created after submission. **It's vital that they be properly
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.
Example:
@ -119,13 +127,14 @@ Example:
## 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
parent::__construct()
with the right parameters. You may choose to take $fields and $actions as arguments if you wish, but $controller and
$name must be passed - their values depend on where the form is instantiated.
with the right parameters. You may choose to take $fields and $actions as
arguments if you wish, but $controller and $name must be passed—their values
depend on where the form is instantiated.
:::php
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
means that the form takes responsibilities from the controller and manage how to parse and use the form
The real difference, however, is that you can then define your controller
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.
**Page.php**
@ -214,7 +224,7 @@ form.
## Readonly
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.
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*
It's recommended you only do this if you've got a lot of presentation text, 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.
It's recommended you do this only if you have a lot of presentation text or
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
class MyForm extends Form {
@ -305,14 +318,16 @@ your project. Here is an example of basic customization:
<% end_if %>
</form>
`$Fields.dataFieldByName(FirstName)` will return the form control contents of `Field()` for the particular field object,
in this case `EmailField->Field()` or `PasswordField->Field()` which returns an `<input>` element with specific markup
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.
`$Fields.dataFieldByName(FirstName)` will return the form control contents of
`Field()` for the particular field object, in this case `EmailField->Field()` or
`PasswordField->Field()` which returns an `<input>` element with specific markup
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
methods of customising the form templates. An example is that you could use `<% loop $Fields %>` instead of specifying
each field manually, as we've done above.
To find more methods, have a look at the `[api:Form]` class and
`[api:FieldList]` class as there is a lot of different methods of customising
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
@ -333,14 +348,15 @@ Each form field is rendered into a form via the
`<div>` as well as a `<label>` element (if applicable).
You can also render each field without these structural elements through the
`[FormField->Field()](api:FormField)` method. In order to influence the form
rendering, overloading these two methods is a good start.
`[FormField->Field()](api:FormField)` method. To influence form rendering,
overriding these two methods is a good start.
In addition, most form fields are rendered through SilverStripe templates, e.g.
`TextareaField` is rendered via `framework/templates/forms/TextareaField.ss`.
In addition, most form fields are rendered through SilverStripe templates; for
example, `TextareaField` is rendered via
`framework/templates/forms/TextareaField.ss`.
These templates can be overwritten globally by placing a template with the same
name in your `mysite` directory, or set on a form field instance via anyone of
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 any of
these methods:
- FormField->setTemplate()
@ -358,7 +374,7 @@ by adding a hidden *SecurityID* parameter to each form. See
[secure-development](/topics/security) for details.
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
$myForm->setFormMethod('POST');
@ -391,10 +407,170 @@ Adds a new text field called FavouriteColour next to the Content field in the CM
:::php
$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
* [Form Field Types](/reference/form-field-types)
* [MultiForm Module](http://silverstripe.org/multi-form-module)
* Model Validation with [api:DataObject->validate()]
## API Documentation

View File

@ -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
* [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
* [Form Validation](form-validation): Built-in validation on form fields, and how to extend it
* [Forms](forms): Create your own form, add fields and create your own form template using the existing `Form` class
* [Forms & form validation](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
* [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
* [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?
* [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
* [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
* [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
* [Widgets](widgets): Small feature blocks which can be placed on a page by the CMS editor, also outlines how to create and add widgets
* [Theme Development](theme-development): Package templates, images and CSS to a reusable theme
* [Using Themes](themes): How to download and install themes
* [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

View File

@ -2,38 +2,42 @@
## Overview
You will often find the need to test your functionality with some consistent data.
If we are testing our code with the same data each time,
we can trust our tests to yeild 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 -
all we have to do is define them, and we have a few ways in which we can do this.
You will often find the need to test your functionality with some consistent
data. If we are testing our code with the same data each time, we can trust our
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 - all we have to do is define them, and
we have a few ways in which we can do this.
## YAML Fixtures
YAML is a markup language which is deliberately simple and easy to read,
so it is ideal for fixture generation.
YAML is a markup language which is deliberately simple and easy to read, so it
is ideal for fixture generation.
Say we have the following two DataObjects:
:::php
class Player extends DataObject {
static $db = array (
private static $db = array (
'Name' => 'Varchar(255)'
);
static $has_one = array(
private static $has_one = array(
'Team' => 'Team'
);
}
class Team extends DataObject {
static $db = array (
private static $db = array (
'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)'
);
static $has_many = array(
private static $has_many = array(
'Players' => 'Player'
);
}
@ -59,31 +63,42 @@ We can represent multiple instances of them in `YAML` as follows:
Name: The Crusaders
Origin: Bay of Plenty
Our `YAML` is broken up into three levels, signified by the indentation of each line.
In the first level of indentation, `Player` and `Team`,
represent the class names of the objects we want to be created for the test.
Our `YAML` is broken up into three levels, signified by the indentation of each
line. In the first level of indentation, `Player` and `Team`, represent the
class names of the objects we want to be created for the test.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers.
These are what you pass as the second argument of `SapphireTest::objFromFixture()`.
Each identifier you specify represents a new object.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are
identifiers. These are what you pass as the second argument of
`SapphireTest::objFromFixture()`. Each identifier you specify represents a new
object.
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,
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`.
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, 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`,
just like how a relationship is always set up.
<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>
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship.
For `many-many` relationships, we specify a comma separated list of values.
This style of relationship declaration can be used for both a `has-one` and a
`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:
:::yml
@ -104,27 +119,97 @@ For example we could just as easily write the above as:
Origin: Bay of Plenty
Players: =>Player.joe,=>Player.jack
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**.
The database is populated by instantiating DataObject objects and setting the fields declared in the YML,
then calling write() on those objects.
This means that any `onBeforeWrite()` or default value logic will be executed as part of 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).
A crucial thing to note is that **the YAML file specifies DataObjects, not
database records**.
The database is populated by instantiating DataObject objects and setting the
fields declared in the YML, then calling write() on those objects. This means
that any `onBeforeWrite()` or default value logic will be executed as part of
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
### Manual Object Creation
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model,
or because the YAML format doesn't allow you to modify all of a model's state.
One common example here is publishing pages (page fixtures aren't published by default).
Sometimes statically defined fixtures don't suffice. This could be because of
the complexity of the tested model, or because the YAML format doesn't allow you
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.
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
class SiteTreeTest extends SapphireTest {
function 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?
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention.
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values,
callbacks on object creation, and dynamic/lazy value setting.
While manually defined fixtures provide full flexibility, they offer very little
in terms of structure and convention. Alternatively, you can use the
`[api:FixtureFactory]` class, which allows you to set default values, callbacks
on object creation, and dynamic/lazy value setting.
<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>
The idea is that rather than instantiating objects directly, we'll have a 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.
The idea is that rather than instantiating objects directly, we'll have a
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

View File

@ -45,7 +45,7 @@ When designing your site you should only need to modify the *mysite*, *themes* a
![](_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
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).

View File

@ -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) {
$this->setField('Filename', $val);

View File

@ -57,20 +57,23 @@ class Folder extends File {
$parentID = 0;
$item = null;
$filter = FileNameFilter::create();
foreach($parts as $part) {
if(!$part) continue; // happens for paths with a trailing slash
$item = DataObject::get_one(
"Folder",
sprintf(
"\"Name\" = '%s' AND \"ParentID\" = %d",
Convert::raw2sql($part),
(int)$parentID
)
);
// Ensure search includes folders with illegal characters removed, but
// err in favour of matching existing folders if $folderPath
// includes illegal characters itself.
$partSafe = $filter->filter($part);
$item = Folder::get()->filter(array(
'ParentID' => $parentID,
'Name' => array($partSafe, $part)
))->first();
if(!$item) {
$item = new Folder();
$item->ParentID = $parentID;
$item->Name = $part;
$item->Name = $partSafe;
$item->Title = $part;
$item->write();
}
@ -460,7 +463,7 @@ class Folder extends File {
* Get the children of this folder that are also folders.
*/
public function ChildFolders() {
return DataObject::get("Folder", "\"ParentID\" = " . (int)$this->ID);
return Folder::get()->filter('ParentID', $this->ID);
}
/**

View File

@ -134,19 +134,24 @@ class Upload extends Controller {
$file = $nameFilter->filter($tmpFile['name']);
$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)
if(!$this->file) {
$fileClass = File::get_class_for_file_extension(pathinfo($tmpFile['name'], PATHINFO_EXTENSION));
if($this->replaceFile) {
$this->file = File::get()
$this->file = new $fileClass();
}
if(!$this->file->ID && $this->replaceFile) {
$fileClass = $this->file->class;
$file = File::get()
->filter(array(
'ClassName' => $fileClass,
'Name' => $fileName,
'ParentID' => $parentFolder ? $parentFolder->ID : 0
))->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)

View File

@ -117,15 +117,15 @@ class DropdownField extends FormField {
protected $disabledItems = array();
/**
* Creates a new dropdown field.
* @param $name The field name
* @param $title The field title
* @param $source An map of the dropdown items
* @param $value The current value
* @param $form The parent form
* @param $emptyString mixed Add an empty selection on to of the {@link $source}-Array (can also be boolean, which
* results in an empty string). Argument is deprecated in 3.1, please use
* {@link setEmptyString()} and/or {@link setHasEmptyDefault(true)} instead.
* @param string $name The field name
* @param string $title The field title
* @param array $source An map of the dropdown items
* @param string $value The current value
* @param Form $form The parent form
* @param string|bool $emptyString Add an empty selection on to of the {@link $source}-Array (can also be
* boolean, which results in an empty string). Argument is deprecated
* in 3.1, please use{@link setEmptyString()} and/or
* {@link setHasEmptyDefault(true)} instead.
*/
public function __construct($name, $title=null, $source=array(), $value='', $form=null, $emptyString=null) {
$this->setSource($source);

View File

@ -698,7 +698,7 @@ class Form extends RequestHandler {
* @return String
*/
public function getAttribute($name) {
return @$this->attributes[$name];
if(isset($this->attributes[$name])) return $this->attributes[$name];
}
public function getAttributes() {
@ -1093,13 +1093,17 @@ class Form extends RequestHandler {
return $this->messageType;
}
/**
* @return string
*/
protected function getMessageFromSession() {
if($this->message || $this->messageType) {
return $this->message;
}
} else {
$this->message = Session::get("FormInfo.{$this->FormName()}.formError.message");
$this->messageType = Session::get("FormInfo.{$this->FormName()}.formError.type");
return $this->message;
}
/**

View File

@ -416,8 +416,7 @@ class FormField extends RequestHandler {
*/
public function getAttribute($name) {
$attrs = $this->getAttributes();
return @$attrs[$name];
if(isset($attrs[$name])) return $attrs[$name];
}
/**

View File

@ -62,10 +62,10 @@ class HtmlEditorSanitiser {
foreach(explode(',', $validElements) as $validElement) {
if(preg_match($elementRuleRegExp, $validElement, $matches)) {
$prefix = @$matches[1];
$elementName = @$matches[2];
$outputName = @$matches[3];
$attrData = @$matches[4];
$prefix = isset($matches[1]) ? $matches[1] : null;
$elementName = isset($matches[2]) ? $matches[2] : null;
$outputName = isset($matches[3]) ? $matches[3] : null;
$attrData = isset($matches[4]) ? $matches[4] : null;
// Create the new element
$element = new stdClass();
@ -91,10 +91,10 @@ class HtmlEditorSanitiser {
if(preg_match($attrRuleRegExp, $attr, $matches)) {
$attr = new stdClass();
$attrType = @$matches[1];
$attrName = str_replace('::', ':', @$matches[2]);
$prefix = @$matches[3];
$value = @$matches[4];
$attrType = isset($matches[1]) ? $matches[1] : null;
$attrName = isset($matches[2]) ? str_replace('::', ':', $matches[2]) : null;
$prefix = isset($matches[3]) ? $matches[3] : null;
$value = isset($matches[4]) ? $matches[4] : null;
// Required
if($attrType === '!') {

View File

@ -1297,11 +1297,15 @@ class UploadField extends FileField {
$nameFilter = FileNameFilter::create();
$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
$folder = $this->getFolderName();
$exists = false;
foreach(array($originalFile, $filteredFile) as $file) {
if(file_exists(ASSETS_PATH."/$folder/$file")) {
if(file_exists($parentPath.$file)) {
$exists = true;
break;
}

View File

@ -616,7 +616,8 @@ class GridField extends FormField {
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
$html = '';
$data = $request->requestVars();
$fieldData = @$data[$this->getName()];
$name = $this->getName();
$fieldData = isset($data[$name]) ? $data[$name] : null;
// Update state from client
$state = $this->getState(false);

View File

@ -16,7 +16,7 @@
(function($){
var decodePath = function(str) {
return str.replace(/%2C/g,',').replace(/\&amp;/g, '&').trim();
return str.replace(/%2C/g,',').replace(/\&amp;/g, '&').replace(/^\s+|\s+$/g, '');
};
$.extend({

View File

@ -212,13 +212,15 @@ abstract class SS_Database {
switch($changes['command']) {
case 'create':
$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'], $changes['options'],
@$changes['advancedOptions']);
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
);
break;
case 'alter':
$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
$changes['alteredFields'], $changes['alteredIndexes'], $changes['alteredOptions'],
@$changes['advancedOptions']);
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
);
break;
}
}

View File

@ -163,9 +163,13 @@ class SS_HTML4Value extends SS_HTMLValue {
// Reset the document if we're in an invalid state for some reason
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>' .
"<body>$content</body></html>"
);
libxml_clear_errors();
libxml_use_internal_errors($errorState);
return $result;
}
}

View File

@ -33,7 +33,8 @@ class URLSegmentFilter extends Object {
'/[_.]+/u' => '-', // underscores and dots to dashes
'/[^A-Za-z0-9\-]+/u' => '', // remove non-ASCII chars, only allow alphanumeric and dashes
'/[\-]{2,}/u' => '-', // remove duplicate dashes
'/^[\-_]/u' => '', // Remove all leading dashes or underscores
'/^[\-]+/u' => '', // Remove all leading dashes
'/[\-]+$/u' => '' // Remove all trailing dashes
);
/**

View File

@ -229,8 +229,8 @@ class ShortcodeParser {
'text' => $match[0][0],
's' => $match[0][1],
'e' => $match[0][1] + strlen($match[0][0]),
'open' => @$match['open'][0],
'close' => @$match['close'][0],
'open' => isset($match['open'][0]) ? $match['open'][0] : null,
'close' => isset($match['close'][0]) ? $match['close'][0] : null,
'attrs' => $attrs,
'content' => '',
'escaped' => !empty($match['oesc'][0]) || !empty($match['cesc1'][0]) || !empty($match['cesc2'][0])

View File

@ -105,6 +105,11 @@ class ChangePasswordForm extends Form {
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if (isset($_REQUEST['BackURL'])
&& $_REQUEST['BackURL']
// absolute redirection URLs may cause spoofing

View File

@ -1180,10 +1180,10 @@ class Member extends DataObject implements TemplateGlobalProvider {
* editing this member.
*/
public function getCMSFields() {
require_once('Zend/Date.php');
$fields = parent::getCMSFields();
require_once 'Zend/Date.php';
$self = $this;
$this->beforeUpdateCMSFields(function($fields) use ($self) {
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
$password = new ConfirmedPasswordField(
@ -1194,7 +1194,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
true // showOnClick
);
$password->setCanBeEmpty(true);
if(!$this->ID) $password->showOnClick = false;
if( ! $self->ID) $password->showOnClick = false;
$mainFields->replaceField('Password', $password);
$mainFields->replaceField('Locale', new DropdownField(
@ -1210,11 +1210,14 @@ class Member extends DataObject implements TemplateGlobalProvider {
$mainFields->removeByName('PasswordExpiry');
$mainFields->removeByName('LockedOutUntil');
if(!self::config()->lock_out_after_incorrect_logins) {
if( ! $self->config()->lock_out_after_incorrect_logins) {
$mainFields->removeByName('FailedLoginCount');
}
$mainFields->removeByName('Salt');
$mainFields->removeByName('NumVisit');
$mainFields->makeFieldReadonly('LastVisited');
$fields->removeByName('Subscriptions');
@ -1239,17 +1242,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
)
);
// Add permission field (readonly to avoid complicated group assignment logic).
// This should only be available for existing records, as new records start
// with no permissions until they have a group assignment anyway.
if($this->ID) {
if($self->ID) {
$permissionsField = new PermissionCheckboxSetField_Readonly(
'Permissions',
false,
'Permission',
'GroupID',
// 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->addFieldToTab('Root.Permissions', $permissionsField);
@ -1259,44 +1263,42 @@ class Member extends DataObject implements TemplateGlobalProvider {
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
if($permissionsTab) $permissionsTab->addExtraClass('readonly');
$localDateFormat = Zend_Locale_Data::getContent(new Zend_Locale($this->Locale), 'date', 'short');
// Ensure short dates always use four digit dates to avoid confusion
$localDateFormat = preg_replace('/(^|[^y])yy($|[^y])/', '$1yyyy$2', $localDateFormat);
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
$dateFormatMap = array(
$this->DateFormat => Zend_Date::now()->toString($this->DateFormat),
$localDateFormat => Zend_Date::now()->toString($localDateFormat),
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/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(
$dateFormatField = new MemberDatetimeOptionsetField(
'DateFormat',
$this->fieldLabel('DateFormat'),
$self->fieldLabel('DateFormat'),
$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(
$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' => Zend_Date::now()->toString('H:mm'),
);
$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
$mainFields->push(
$timeFormatField = new MemberDatetimeOptionsetField(
'TimeFormat',
$this->fieldLabel('TimeFormat'),
$self->fieldLabel('TimeFormat'),
$timeFormatMap
)
);
$timeFormatField->setValue($this->TimeFormat);
$timeFormatField->setValue($self->TimeFormat);
});
$this->extend('updateCMSFields', $fields);
return $fields;
return parent::getCMSFields();
}
/**

View File

@ -1,6 +1,14 @@
<?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
* @subpackage security
*/
@ -256,9 +264,12 @@ JS
/**
* Forgot password form handler method
*
* This method is called when the user clicks on "I've lost my password"
* Forgot password form handler method.
* 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
*/
@ -267,6 +278,12 @@ JS
$SQL_email = $SQL_data['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) {
$token = $member->generateAutologinTokenAndStoreHash();

View File

@ -184,14 +184,14 @@ class CmsUiContext extends BehatContext {
* @When /^I expand the "([^"]*)" CMS Panel$/
*/
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();
$panel_toggle_element = $page->find('css', '.cms-content > .cms-panel > .cms-panel-toggle > .toggle-expand');
assertNotNull($panel_toggle_element, 'Panel toggle not found');
if ($panel_toggle_element->isVisible()) {
$panel_toggle_element->click();
$toggle_elements = $page->findAll('css', '.toggle-expand');
assertNotNull($toggle_elements, 'Panel toggle not found');
foreach($toggle_elements as $toggle){
if($toggle->isVisible()){
$toggle->click();
}
}
}

Binary file not shown.

View File

@ -12,6 +12,58 @@ class FixtureBlueprintTest extends SapphireTest {
'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() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$obj = $blueprint->createObject('one');
@ -28,6 +80,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->assertEquals('My Name', $obj->Name);
}
public function testCreateWithRelationship() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
@ -127,7 +180,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->assertEquals(99, $obj->ID);
}
function testCallbackOnBeforeCreate() {
public function testCallbackOnBeforeCreate() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$this->_called = 0;
$self = $this;
@ -144,7 +197,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->_called = 0;
}
function testCallbackOnAfterCreate() {
public function testCallbackOnAfterCreate() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$this->_called = 0;
$self = $this;
@ -161,7 +214,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->_called = 0;
}
function testDefineWithDefaultCustomSetters() {
public function testDefineWithDefaultCustomSetters() {
$blueprint = new FixtureBlueprint(
'FixtureFactoryTest_DataObject',
null,

View File

@ -1,4 +1,5 @@
<?php
/**
* @package framework
* @subpackage tests
@ -151,19 +152,37 @@ class FixtureFactoryTest extends SapphireTest {
}
/**
* @package framework
* @subpackage tests
*/
class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
"Name" => "Varchar"
);
private static $many_many = array(
"ManyMany" => "FixtureFactoryTest_DataObjectRelation"
);
private static $many_many_extraFields = array(
"ManyMany" => array(
"Label" => "Varchar"
)
);
}
/**
* @package framework
* @subpackage tests
*/
class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly {
private static $db = array(
"Name" => "Varchar"
);
private static $belongs_many_many = array(
"TestParent" => "FixtureFactoryTest_DataObject"
);

View File

@ -338,4 +338,21 @@ class FolderTest extends SapphireTest {
))->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);
}
}

View File

@ -378,6 +378,93 @@ class UploadTest extends SapphireTest {
$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 {

View File

@ -342,7 +342,6 @@ class SQLQueryTest extends SapphireTest {
);
}
public function testSetWhereAny() {
$query = new SQLQuery();
$query->setFrom('MyTable');
@ -352,7 +351,6 @@ class SQLQueryTest extends SapphireTest {
}
public function testSelectFirst() {
// Test first from sequence
$query = new SQLQuery();
$query->setFrom('"SQLQueryTest_DO"');
@ -398,7 +396,6 @@ class SQLQueryTest extends SapphireTest {
}
public function testSelectLast() {
// Test last in sequence
$query = new SQLQuery();
$query->setFrom('"SQLQueryTest_DO"');

View File

@ -41,8 +41,8 @@ class URLSegmentFilterTest extends SapphireTest {
public function testReplacesCommonNonAsciiCharacters() {
$f = new URLSegmentFilter();
$this->assertEquals(
urlencode('aa1-'),
$f->filter('Aa1~!@#$%^*()_`-=;\':"[]\{}|,./<>?')
urlencode('aa1-a'),
$f->filter('Aa1~!@#$%^*()_`-=;\':"[]\{}|,./<>?a')
);
}
@ -77,4 +77,14 @@ class URLSegmentFilterTest extends SapphireTest {
$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--'));
}
}

View File

@ -26,8 +26,8 @@ class BasicAuthTest extends FunctionalTest {
}
public function testBasicAuthEnabledWithoutLogin() {
$origUser = @$_SERVER['PHP_AUTH_USER'];
$origPw = @$_SERVER['PHP_AUTH_PW'];
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -40,8 +40,8 @@ class BasicAuthTest extends FunctionalTest {
}
public function testBasicAuthDoesntCallActionOrFurtherInitOnAuthFailure() {
$origUser = @$_SERVER['PHP_AUTH_USER'];
$origPw = @$_SERVER['PHP_AUTH_PW'];
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -60,8 +60,8 @@ class BasicAuthTest extends FunctionalTest {
}
public function testBasicAuthEnabledWithPermission() {
$origUser = @$_SERVER['PHP_AUTH_USER'];
$origPw = @$_SERVER['PHP_AUTH_PW'];
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
@ -83,8 +83,8 @@ class BasicAuthTest extends FunctionalTest {
}
public function testBasicAuthEnabledWithoutPermission() {
$origUser = @$_SERVER['PHP_AUTH_USER'];
$origPw = @$_SERVER['PHP_AUTH_PW'];
$origUser = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null;
$origPw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null;
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword';

View File

@ -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
*/
@ -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
* @subpackage tests

View File

@ -213,6 +213,9 @@ class SecurityTest extends FunctionalTest {
public function testChangePasswordFromLostPassword() {
$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');
@ -243,6 +246,10 @@ class SecurityTest extends FunctionalTest {
$goodResponse = $this->doTestLoginForm('sam@silverstripe.com' , 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode());
$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() {