API Refactor Form request handling into FormRequestHandler

API Add HasRequestHandler interface
API Refactor Link() and url handling behaviour from Controller into RequestHandler
API RequestHandler classes now must define url_segment to have a default Link()
API Clean up redirectBack()
This commit is contained in:
Damian Mooyman 2017-03-02 15:24:38 +13:00
parent f520a846c5
commit 0c41a97a8b
61 changed files with 1561 additions and 1220 deletions

View File

@ -101,9 +101,9 @@ class CMSBatchActionHandler extends RequestHandler
); );
} }
public function Link() public function Link($action = null)
{ {
return Controller::join_links($this->parentController->Link(), $this->urlSegment); return Controller::join_links($this->parentController->Link(), $this->urlSegment, $action, '/');
} }
/** /**

View File

@ -1063,6 +1063,16 @@ now generally safer to use the default inherited config, where in the past you w
* Falsey config values (null, 0, false, etc) can now replace non-falsey values. * Falsey config values (null, 0, false, etc) can now replace non-falsey values.
* Introduced new ModuleLoader manifest, which allows modules to be found via composer name. * Introduced new ModuleLoader manifest, which allows modules to be found via composer name.
E.g. `$cms = ModuleLoader::instance()->getManifest()->getModule('silverstripe/cms')` E.g. `$cms = ModuleLoader::instance()->getManifest()->getModule('silverstripe/cms')`
* Certain methods have been moved from `Controller` to `RequestHandler`:
* `Link`
* `redirect`
* `redirectBack`
* `RequestHandler` link and redirection behaviour has been enhanced slightly:
* `Link` now relies on the `url_segment` handler being provided for the class.
If left unset, this will raise an error.
* `getBackURL` and `getReturnReferer` have been added to safely inspect the current request
to see if there is a url to redirect back to.
#### <a name="overview-general-removed"></a>General and Core Removed API #### <a name="overview-general-removed"></a>General and Core Removed API
@ -1100,6 +1110,8 @@ now generally safer to use the default inherited config, where in the past you w
[silverstripe-archive/bbcodeparser](https://github.com/silverstripe-archive/silverstripe-bbcodeparser) [silverstripe-archive/bbcodeparser](https://github.com/silverstripe-archive/silverstripe-bbcodeparser)
* Removed `ViewableData::ThemeDir`. Use `ThemeResourceLoader::findThemedResource` in conjunction with `SSViewer::get_themes` instead. * Removed `ViewableData::ThemeDir`. Use `ThemeResourceLoader::findThemedResource` in conjunction with `SSViewer::get_themes` instead.
* Removed `Config::FIRST_SET` and `Config::INHERITED` * Removed `Config::FIRST_SET` and `Config::INHERITED`
* Removed `RequestHandler.require_allowed_actions`. This is now fixed to on and cannot be
disabled.
#### <a name="overview-general-deprecated"></a>General and Core Deprecated API #### <a name="overview-general-deprecated"></a>General and Core Deprecated API
@ -1415,8 +1427,10 @@ handle field-level and form-level messages. This has the following properties:
* `getMessageCastingHelper` retrieves the DBField cast to use for the appropriate message cast * `getMessageCastingHelper` retrieves the DBField cast to use for the appropriate message cast
* `getSchemaMessage` encodes this message for form schema use in ReactJS. * `getSchemaMessage` encodes this message for form schema use in ReactJS.
`Form` methods have been changed: `Form` behaviour methods have been changed:
* __construct() now allows a RequestHandler to be passed as a first argument, rather than a Controller.
in addition this argument is now optional. This allows forms to be constructed as a model only.
* `validate` is replaced with `validationResult` instead, which returns a `ValidationResult` instance. * `validate` is replaced with `validationResult` instead, which returns a `ValidationResult` instance.
This is no longer automatically persisted in the state by default, unless a redirection occurs. This is no longer automatically persisted in the state by default, unless a redirection occurs.
You can also save any response in the state by manually invoking `saveFormState` inside a custom You can also save any response in the state by manually invoking `saveFormState` inside a custom
@ -1431,6 +1445,15 @@ handle field-level and form-level messages. This has the following properties:
* `getAjaxErrorResponse` and `getRedirectReferer` created to simplify `getValidationErrorResponse` * `getAjaxErrorResponse` and `getRedirectReferer` created to simplify `getValidationErrorResponse`
* `addErrorMessage` removed. Users can either use `sessionMessage` or `sessionError` to add a * `addErrorMessage` removed. Users can either use `sessionMessage` or `sessionError` to add a
form level message, throw a ValidationException during submission, or add a custom validator. form level message, throw a ValidationException during submission, or add a custom validator.
* `Form` is no longer a `RequestHandler`, but implements the `HasRequestHandler` interface and returns
a `FormRequestHandler` instance from `getRequestHandler()`. the `Form` constructor no longer has
any mandatory parameters, and the first parameter allows a non-`Controller` `RequestHandler` to be
passed. Certain methods have been moved from `Form` to `FormRequestHandler`:
* `buttonClicked`
* `checkAccessAction`
* `handleField`
* `httpSubmission`
* `Link`
`Validator` methods have changed: `Validator` methods have changed:
@ -1490,6 +1513,8 @@ New `TimeField` methods replace `getConfig()` / `setConfig()`
* `resetValidation` * `resetValidation`
* `messageForForm` * `messageForForm`
* `addErrorMessage` * `addErrorMessage`
* `testSubmission`
* `testAjaxSubmission`
* Removed `Validator::requireField()` method. * Removed `Validator::requireField()` method.
* Removed `ValidationResult` (see above for replacements): * Removed `ValidationResult` (see above for replacements):
* `messageList` * `messageList`

View File

@ -132,17 +132,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
$this->extend('onAfterInit'); $this->extend('onAfterInit');
} }
/**
* Returns a link to this controller. Overload with your own Link rules if they exist.
*
* @param string $action Optional action
* @return string
*/
public function Link($action = null)
{
return Controller::join_links(ClassInfo::shortName($this), $action, '/');
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* *
@ -646,56 +635,9 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
. "; now trying to direct to $url", E_USER_WARNING); . "; now trying to direct to $url", E_USER_WARNING);
return null; return null;
} }
$response = parent::redirect($url, $code);
// Attach site-root to relative links, if they have a slash in them $this->setResponse($response);
if ($url=="" || $url[0]=='?' || (substr($url, 0, 4) != "http" && $url[0] != "/" && strpos($url, '/') !== false)) { return $response;
$url = Director::baseURL() . $url;
}
return $this->getResponse()->redirect($url, $code);
}
/**
* Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
* This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
* location.href in IE). If none of the two variables is available, it will redirect to the base
* URL (see {@link Director::baseURL()}).
*
* @uses redirect()
*
* @return bool|HTTPResponse
*/
public function redirectBack()
{
// Don't cache the redirect back ever
HTTP::set_cache_age(0);
$url = null;
// In edge-cases, this will be called outside of a handleRequest() context; in that case,
// redirect to the homepage - don't break into the global state at this stage because we'll
// be calling from a test context or something else where the global state is inappropraite
if ($this->getRequest()) {
if ($this->getRequest()->requestVar('BackURL')) {
$url = $this->getRequest()->requestVar('BackURL');
} elseif ($this->getRequest()->isAjax() && $this->getRequest()->getHeader('X-Backurl')) {
$url = $this->getRequest()->getHeader('X-Backurl');
} elseif ($this->getRequest()->getHeader('Referer')) {
$url = $this->getRequest()->getHeader('Referer');
}
}
if (!$url) {
$url = Director::baseURL();
}
// absolute redirection URLs not located on this site may cause phishing
if (Director::is_site_url($url)) {
$url = Director::absoluteURL($url, true);
return $this->redirect($url);
} else {
return false;
}
} }
/** /**

View File

@ -424,10 +424,6 @@ class Director implements TemplateGlobalProvider
{ {
$rules = Director::config()->uninherited('rules'); $rules = Director::config()->uninherited('rules');
if (isset($_REQUEST['debug'])) {
Debug::show($rules);
}
foreach ($rules as $pattern => $controllerOptions) { foreach ($rules as $pattern => $controllerOptions) {
if (is_string($controllerOptions)) { if (is_string($controllerOptions)) {
if (substr($controllerOptions, 0, 2) == '->') { if (substr($controllerOptions, 0, 2) == '->') {

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Control;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
@ -12,6 +13,7 @@ use SilverStripe\View\Requirements;
*/ */
class HTTPResponse class HTTPResponse
{ {
use Injectable;
/** /**
* @var array * @var array

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\Control;
/**
* Indicator for a class which cannot handle requests directly, but is able to
* generate a delegate for those requests.
*/
interface HasRequestHandler
{
/**
* @return RequestHandler
*/
public function getRequestHandler();
}

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Object; use SilverStripe\Core\Object;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
@ -47,6 +48,13 @@ use BadMethodCallException;
*/ */
class RequestHandler extends ViewableData class RequestHandler extends ViewableData
{ {
/**
* Optional url_segment for this request handler
*
* @config
* @var string|null
*/
private static $url_segment = null;
/** /**
* @var HTTPRequest $request The request object that the controller was called with. * @var HTTPRequest $request The request object that the controller was called with.
@ -110,16 +118,6 @@ class RequestHandler extends ViewableData
*/ */
private static $allowed_actions = null; private static $allowed_actions = null;
/**
* @config
* @var boolean Enforce presence of $allowed_actions when checking acccess.
* Defaults to TRUE, meaning all URL actions will be denied.
* When set to FALSE, the controller will allow *all* public methods to be called.
* In most cases this isn't desireable, and in fact a security risk,
* since some helper methods can cause side effects which shouldn't be exposed through URLs.
*/
private static $require_allowed_actions = true;
public function __construct() public function __construct()
{ {
$this->brokenOnConstruct = false; $this->brokenOnConstruct = false;
@ -235,7 +233,11 @@ class RequestHandler extends ViewableData
// empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller // empty rule ourselves, to prevent infinite loops. Also prevent further handling of controller
// actions which return themselves to avoid infinite loops. // actions which return themselves to avoid infinite loops.
$matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']); $matchedRuleWasEmpty = $request->isEmptyPattern($match['rule']);
if ($this !== $result && !$matchedRuleWasEmpty && ($result instanceof RequestHandler)) { if ($this !== $result && !$matchedRuleWasEmpty && ($result instanceof RequestHandler || $result instanceof HasRequestHandler)) {
// Expose delegated request handler
if ($result instanceof HasRequestHandler) {
$result = $result->getRequestHandler();
}
$returnValue = $result->handleRequest($request, $model); $returnValue = $result->handleRequest($request, $model);
// Array results can be used to handle // Array results can be used to handle
@ -485,7 +487,7 @@ class RequestHandler extends ViewableData
$isAllowed = false; $isAllowed = false;
} elseif ($allowedActions === null) { } elseif ($allowedActions === null) {
// If undefined, allow action based on configuration // If undefined, allow action based on configuration
$isAllowed = !Config::inst()->get('SilverStripe\\Control\\RequestHandler', 'require_allowed_actions'); $isAllowed = false;
} }
// If we don't have a match in allowed_actions, // If we don't have a match in allowed_actions,
@ -508,7 +510,6 @@ class RequestHandler extends ViewableData
*/ */
public function httpError($errorCode, $errorMessage = null) public function httpError($errorCode, $errorMessage = null)
{ {
$request = $this->getRequest(); $request = $this->getRequest();
// Call a handler method such as onBeforeHTTPError404 // Call a handler method such as onBeforeHTTPError404
@ -527,7 +528,7 @@ class RequestHandler extends ViewableData
* {@link handleAction()} or {@link handleRequest()} have been called, * {@link handleAction()} or {@link handleRequest()} have been called,
* which adds a reference to an actual {@link HTTPRequest} object. * which adds a reference to an actual {@link HTTPRequest} object.
* *
* @return HTTPRequest|NullHTTPRequest * @return HTTPRequest
*/ */
public function getRequest() public function getRequest()
{ {
@ -546,4 +547,122 @@ class RequestHandler extends ViewableData
$this->request = $request; $this->request = $request;
return $this; return $this;
} }
/**
* Returns a link to this controller. Overload with your own Link rules if they exist.
*
* @param string $action Optional action
* @return string
*/
public function Link($action = null)
{
// Check configured url_segment
$url = $this->config()->get('url_segment');
if ($url) {
return Controller::join_links($url, $action, '/');
}
// no link defined by default
trigger_error(
'Request handler '.get_class($this). ' does not have a url_segment defined. '.
'Relying on this link may be an application error',
E_USER_WARNING
);
return null;
}
/**
* Redirect to the given URL.
*
* @param string $url
* @param int $code
* @return HTTPResponse
*/
public function redirect($url, $code = 302)
{
$url = Director::absoluteURL($url);
$response = new HTTPResponse();
return $response->redirect($url, $code);
}
/**
* Safely get the value of the BackURL param, if provided via querystring / posted var
*
* @return string
*/
public function getBackURL()
{
$request = $this->getRequest();
if (!$request) {
return null;
}
$backURL = $request->requestVar('BackURL');
// Fall back to X-Backurl header
if (!$backURL && $request->isAjax() && $request->getHeader('X-Backurl')) {
$backURL = $request->getHeader('X-Backurl');
}
if (!$backURL) {
return null;
}
if (Director::is_site_url($backURL)) {
return $backURL;
}
return null;
}
/**
* Returns the referer, if it is safely validated as an internal URL
* and can be redirected to.
*
* @internal called from {@see Form::getValidationErrorResponse}
* @return string|null
*/
public function getReturnReferer()
{
$referer = $this->getReferer();
if ($referer && Director::is_site_url($referer)) {
return $referer;
}
return null;
}
/**
* Get referer
*
* @return string
*/
public function getReferer()
{
$request = $this->getRequest();
if (!$request) {
return null;
}
return $request->getHeader('Referer');
}
/**
* Redirect back. Uses either the HTTP-Referer or a manually set request-variable called "BackURL".
* This variable is needed in scenarios where HTTP-Referer is not sent (e.g when calling a page by
* location.href in IE). If none of the two variables is available, it will redirect to the base
* URL (see {@link Director::baseURL()}).
*
* @uses redirect()
*
* @return HTTPResponse
*/
public function redirectBack()
{
// Don't cache the redirect back ever
HTTP::set_cache_age(0);
// Prefer to redirect to ?BackURL, but fall back to Referer header
// As a last resort redirect to base url
$url = $this->getBackURL()
?: $this->getReturnReferer()
?: Director::baseURL();
// Only direct to absolute urls
$url = Director::absoluteURL($url);
return $this->redirect($url);
}
} }

View File

@ -340,4 +340,22 @@ class ClassInfo
$reflection = new ReflectionClass($nameOrObject); $reflection = new ReflectionClass($nameOrObject);
return $reflection->getShortName(); return $reflection->getShortName();
} }
/**
* Helper to determine if the given object has a method
*
* @param object $object
* @param string $method
* @return bool
*/
public static function hasMethod($object, $method)
{
if (empty($object)) {
return false;
}
if (method_exists($object, $method)) {
return true;
}
return method_exists($object, 'hasMethod') && $object->hasMethod($method);
}
} }

View File

@ -2,26 +2,22 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use SilverStripe\Control\Controller; use SilverStripe\Control\HasRequestHandler;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTP; use SilverStripe\Control\HTTP;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\NullSecurityToken; use SilverStripe\Security\NullSecurityToken;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\ViewableData;
/** /**
* Base class for all forms. * Base class for all forms.
@ -64,10 +60,15 @@ use SilverStripe\View\SSViewer;
* For example, the "URLSegment" field in a standard CMS form would be * For example, the "URLSegment" field in a standard CMS form would be
* accessible through "admin/EditForm/field/URLSegment/FieldHolder". * accessible through "admin/EditForm/field/URLSegment/FieldHolder".
*/ */
class Form extends RequestHandler class Form extends ViewableData implements HasRequestHandler
{ {
use FormMessage; use FormMessage;
/**
* Default form Name property
*/
const DEFAULT_NAME = 'Form';
/** /**
* Form submission data is URL encoded * Form submission data is URL encoded
*/ */
@ -97,7 +98,9 @@ class Form extends RequestHandler
protected $actions; protected $actions;
/** /**
* @var Controller * Parent (optional) request handler
*
* @var RequestHandler
*/ */
protected $controller; protected $controller;
@ -112,7 +115,8 @@ class Form extends RequestHandler
protected $validator; protected $validator;
/** /**
* @var callable {@see setValidationResponseCallback()} * @see setValidationResponseCallback()
* @var callable
*/ */
protected $validationResponseCallback; protected $validationResponseCallback;
@ -127,7 +131,9 @@ class Form extends RequestHandler
protected $strictFormMethodCheck = false; protected $strictFormMethodCheck = false;
/** /**
* @var DataObject|null $record Populated by {@link loadDataFrom()}. * Populated by {@link loadDataFrom()}.
*
* @var DataObject|null
*/ */
protected $record; protected $record;
@ -135,7 +141,7 @@ class Form extends RequestHandler
* Keeps track of whether this form has a default action or not. * Keeps track of whether this form has a default action or not.
* Set to false by $this->disableDefaultAction(); * Set to false by $this->disableDefaultAction();
* *
* @var boolean * @var bool
*/ */
protected $hasDefaultAction = true; protected $hasDefaultAction = true;
@ -167,11 +173,6 @@ class Form extends RequestHandler
*/ */
protected $template; protected $template;
/**
* @var callable|null
*/
protected $buttonClickedFunc;
/** /**
* Should we redirect the user back down to the * Should we redirect the user back down to the
* the form on validation errors rather then just the page * the form on validation errors rather then just the page
@ -191,7 +192,9 @@ class Form extends RequestHandler
protected $securityToken = null; protected $securityToken = null;
/** /**
* @var array $extraClasses List of additional CSS classes for the form tag. * List of additional CSS classes for the form tag.
*
* @var array
*/ */
protected $extraClasses = array(); protected $extraClasses = array();
@ -207,8 +210,10 @@ class Form extends RequestHandler
protected $encType; protected $encType;
/** /**
* @var array Any custom form attributes set through {@link setAttributes()}. * Any custom form attributes set through {@link setAttributes()}.
* Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them. * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
*
* @var array
*/ */
protected $attributes = array(); protected $attributes = array();
@ -217,12 +222,10 @@ class Form extends RequestHandler
*/ */
protected $validationExemptActions = array(); protected $validationExemptActions = array();
private static $allowed_actions = array( /**
'handleField', * @config
'httpSubmission', * @var array
'forTemplate', */
);
private static $casting = array( private static $casting = array(
'AttributesHTML' => 'HTMLFragment', 'AttributesHTML' => 'HTMLFragment',
'FormAttributes' => 'HTMLFragment', 'FormAttributes' => 'HTMLFragment',
@ -236,14 +239,19 @@ class Form extends RequestHandler
private $templateHelper = null; private $templateHelper = null;
/** /**
* @ignore * HTML ID for this form.
*
* @var string
*/ */
private $htmlID = null; private $htmlID = null;
/** /**
* @ignore * Custom form action path, if not linking to itself.
* E.g. could be used to post to an external link
*
* @var string
*/ */
private $formActionPath = false; protected $formActionPath = false;
/** /**
* @var bool * @var bool
@ -253,15 +261,20 @@ class Form extends RequestHandler
/** /**
* Create a new form, with the given fields an action buttons. * Create a new form, with the given fields an action buttons.
* *
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param RequestHandler $controller Optional parent request handler
* @param string $name The method on the controller that will return this form object. * @param string $name The method on the controller that will return this form object.
* @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects. * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
* @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
* {@link FormAction} objects * {@link FormAction} objects
* @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields}) * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
*/ */
public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null) public function __construct(
{ RequestHandler $controller = null,
$name = self::DEFAULT_NAME,
FieldList $fields = null,
FieldList $actions = null,
Validator $validator = null
) {
parent::__construct(); parent::__construct();
$fields->setForm($this); $fields->setForm($this);
@ -269,13 +282,9 @@ class Form extends RequestHandler
$this->fields = $fields; $this->fields = $fields;
$this->actions = $actions; $this->actions = $actions;
$this->controller = $controller; $this->setController($controller);
$this->setName($name); $this->setName($name);
if (!$this->controller) {
user_error("$this->class form created without a controller", E_USER_ERROR);
}
// Form validation // Form validation
$this->validator = ($validator) ? $validator : new RequiredFields(); $this->validator = ($validator) ? $validator : new RequiredFields();
$this->validator->setForm($this); $this->validator->setForm($this);
@ -285,8 +294,7 @@ class Form extends RequestHandler
// Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
// method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object. // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
if (method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod') if (ClassInfo::hasMethod($controller, 'securityTokenEnabled')) {
&& $controller->hasMethod('securityTokenEnabled'))) {
$securityEnabled = $controller->securityTokenEnabled(); $securityEnabled = $controller->securityTokenEnabled();
} else { } else {
$securityEnabled = SecurityToken::is_enabled(); $securityEnabled = SecurityToken::is_enabled();
@ -297,18 +305,9 @@ class Form extends RequestHandler
$this->setupDefaultClasses(); $this->setupDefaultClasses();
} }
/**
* @var array
*/
private static $url_handlers = array(
'field/$FieldName!' => 'handleField',
'POST ' => 'httpSubmission',
'GET ' => 'httpSubmission',
'HEAD ' => 'httpSubmission',
);
/** /**
* Load form state from session state * Load form state from session state
*
* @return $this * @return $this
*/ */
public function restoreFormState() public function restoreFormState()
@ -329,11 +328,14 @@ class Form extends RequestHandler
/** /**
* Flush persistant form state details * Flush persistant form state details
*
* @return $this
*/ */
public function clearFormState() public function clearFormState()
{ {
Session::clear("FormInfo.{$this->FormName()}.result"); Session::clear("FormInfo.{$this->FormName()}.result");
Session::clear("FormInfo.{$this->FormName()}.data"); Session::clear("FormInfo.{$this->FormName()}.data");
return $this;
} }
/** /**
@ -350,10 +352,12 @@ class Form extends RequestHandler
* Store the given form data in the session * Store the given form data in the session
* *
* @param array $data * @param array $data
* @return $this
*/ */
public function setSessionData($data) public function setSessionData($data)
{ {
Session::set("FormInfo.{$this->FormName()}.data", $data); Session::set("FormInfo.{$this->FormName()}.data", $data);
return $this;
} }
/** /**
@ -374,6 +378,7 @@ class Form extends RequestHandler
* Sets the ValidationResult in the session to be used with the next view of this form. * Sets the ValidationResult in the session to be used with the next view of this form.
* @param ValidationResult $result The result to save * @param ValidationResult $result The result to save
* @param bool $combineWithExisting If true, then this will be added to the existing result. * @param bool $combineWithExisting If true, then this will be added to the existing result.
* @return $this
*/ */
public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false) public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
{ {
@ -392,12 +397,19 @@ class Form extends RequestHandler
// Serialise // Serialise
$resultData = $result ? serialize($result) : null; $resultData = $result ? serialize($result) : null;
Session::set("FormInfo.{$this->FormName()}.result", $resultData); Session::set("FormInfo.{$this->FormName()}.result", $resultData);
return $this;
} }
/**
* Clear form message (and in session)
*
* @return $this
*/
public function clearMessage() public function clearMessage()
{ {
$this->setMessage(null); $this->setMessage(null);
$this->clearFormState(); $this->clearFormState();
return $this;
} }
/** /**
@ -467,175 +479,6 @@ class Form extends RequestHandler
} }
} }
/**
* Handle a form submission. GET and POST requests behave identically.
* Populates the form with {@link loadDataFrom()}, calls {@link validate()},
* and only triggers the requested form action/method
* if the form is valid.
*
* @param HTTPRequest $request
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function httpSubmission($request)
{
// Strict method check
if ($this->strictFormMethodCheck) {
// Throws an error if the method is bad...
if ($this->formMethod != $request->httpMethod()) {
$response = Controller::curr()->getResponse();
$response->addHeader('Allow', $this->formMethod);
$this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
}
// ...and only uses the variables corresponding to that method type
$vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
} else {
$vars = $request->requestVars();
}
// Ensure we only process saveable fields (non structural, readonly, or disabled)
$allowedFields = array_keys($this->Fields()->saveableFields());
// Populate the form
$this->loadDataFrom($vars, true, $allowedFields);
// Protection against CSRF attacks
// @todo Move this to SecurityTokenField::validate()
$token = $this->getSecurityToken();
if (! $token->checkRequest($request)) {
$securityID = $token->getName();
if (empty($vars[$securityID])) {
$this->httpError(400, _t(
"Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button, ".
"refresh your browser, and try again."
));
} else {
// Clear invalid token on refresh
$this->clearFormState();
$data = $this->getData();
unset($data[$securityID]);
$this->setSessionData($data);
$this->sessionError(_t(
"Form.CSRF_EXPIRED_MESSAGE",
"Your session has expired. Please re-submit the form."
));
// Return the user
return $this->controller->redirectBack();
}
}
// Determine the action button clicked
$funcName = null;
foreach ($vars as $paramName => $paramVal) {
if (substr($paramName, 0, 7) == 'action_') {
// Break off querystring arguments included in the action
if (strpos($paramName, '?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array();
parse_str($paramVars, $newRequestParams);
$vars = array_merge((array)$vars, (array)$newRequestParams);
}
// Cleanup action_, _x and _y from image fields
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
break;
}
}
// If the action wasn't set, choose the default on the form.
if (!isset($funcName) && $defaultAction = $this->defaultAction()) {
$funcName = $defaultAction->actionName();
}
if (isset($funcName)) {
$this->setButtonClicked($funcName);
}
// Permission checks (first on controller, then falling back to form)
if (// Ensure that the action is actually a button or method on the form,
// and not just a method on the controller.
$this->controller->hasMethod($funcName)
&& !$this->controller->checkAccessAction($funcName)
// If a button exists, allow it on the controller
// buttonClicked() validates that the action set above is valid
&& !$this->buttonClicked()
) {
return $this->httpError(
403,
sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
);
} elseif ($this->hasMethod($funcName)
&& !$this->checkAccessAction($funcName)
// No checks for button existence or $allowed_actions is performed -
// all form methods are callable (e.g. the legacy "callfieldmethod()")
) {
return $this->httpError(
403,
sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
);
}
// Action handlers may throw ValidationExceptions.
try {
// Or we can use the Valiator attached to the form
$result = $this->validationResult();
if (!$result->isValid()) {
return $this->getValidationErrorResponse($result);
}
// First, try a handler method on the controller (has been checked for allowed_actions above already)
if ($this->controller->hasMethod($funcName)) {
return $this->controller->$funcName($vars, $this, $request);
}
// Otherwise, try a handler method on the form object.
if ($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this, $request);
}
// Check for inline actions
if ($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
return $field->$funcName($vars, $this, $request);
}
} catch (ValidationException $e) {
// The ValdiationResult contains all the relevant metadata
$result = $e->getResult();
$this->loadMessagesFrom($result);
return $this->getValidationErrorResponse($result);
}
return $this->httpError(404);
}
/**
* @param string $action
* @return bool
*/
public function checkAccessAction($action)
{
if (parent::checkAccessAction($action)) {
return true;
}
$actions = $this->getAllActions();
foreach ($actions as $formAction) {
if ($formAction->actionName() === $action) {
return true;
}
}
// Always allow actions on fields
$field = $this->checkFieldsForAction($this->Fields(), $action);
if ($field && $field->checkAccessAction($action)) {
return true;
}
return false;
}
/** /**
* @return callable * @return callable
*/ */
@ -660,133 +503,6 @@ class Form extends RequestHandler
return $this; return $this;
} }
/**
* Returns the appropriate response up the controller chain
* if {@link validate()} fails (which is checked prior to executing any form actions).
* By default, returns different views for ajax/non-ajax request, and
* handles 'application/json' requests with a JSON object containing the error messages.
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
* and can be overruled by setting {@link $validationResponseCallback}.
*
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getValidationErrorResponse(ValidationResult $result)
{
// Check for custom handling mechanism
$callback = $this->getValidationResponseCallback();
if ($callback && $callbackResponse = call_user_func($callback, $result)) {
return $callbackResponse;
}
// Check if handling via ajax
if ($this->getRequest()->isAjax()) {
return $this->getAjaxErrorResponse($result);
}
// Prior to redirection, persist this result in session to re-display on redirect
$this->setSessionValidationResult($result);
$this->setSessionData($this->getData());
// Determine redirection method
if ($this->getRedirectToFormOnValidationError() && ($pageURL = $this->getRedirectReferer())) {
return $this->controller->redirect($pageURL . '#' . $this->FormName());
}
return $this->controller->redirectBack();
}
/**
* Build HTTP error response for ajax requests
*
* @internal called from {@see Form::getValidationErrorResponse}
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getAjaxErrorResponse(ValidationResult $result)
{
// Ajax form submissions accept json encoded errors by default
$acceptType = $this->getRequest()->getHeader('Accept');
if (strpos($acceptType, 'application/json') !== false) {
// Send validation errors back as JSON with a flag at the start
$response = new HTTPResponse(Convert::array2json($result->getMessages()));
$response->addHeader('Content-Type', 'application/json');
return $response;
}
// Send the newly rendered form tag as HTML
$this->loadMessagesFrom($result);
$response = new HTTPResponse($this->forTemplate());
$response->addHeader('Content-Type', 'text/html');
return $response;
}
/**
* Get referrer to redirect back to and safely validates it
*
* @internal called from {@see Form::getValidationErrorResponse}
* @return string|null
*/
protected function getRedirectReferer()
{
$pageURL = $this->getRequest()->getHeader('Referer');
if (!$pageURL) {
return null;
}
if (!Director::is_site_url($pageURL)) {
return null;
}
// Remove existing pragmas
$pageURL = preg_replace('/(#.*)/', '', $pageURL);
return Director::absoluteURL($pageURL);
}
/**
* Fields can have action to, let's check if anyone of the responds to $funcname them
*
* @param SS_List|array $fields
* @param callable $funcName
* @return FormField
*/
protected function checkFieldsForAction($fields, $funcName)
{
foreach ($fields as $field) {
/** @skipUpgrade */
if (method_exists($field, 'FieldList')) {
if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
return $field;
}
} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
return $field;
}
}
return null;
}
/**
* Handle a field request.
* Uses {@link Form->dataFieldByName()} to find a matching field,
* and falls back to {@link FieldList->fieldByName()} to look
* for tabs instead. This means that if you have a tab and a
* formfield with the same name, this method gives priority
* to the formfield.
*
* @param HTTPRequest $request
* @return FormField
*/
public function handleField($request)
{
$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
if ($field) {
return $field;
} else {
// falling back to fieldByName, e.g. for getting tabs
return $this->Fields()->fieldByName($request->param('FieldName'));
}
}
/** /**
* Convert this form into a readonly form * Convert this form into a readonly form
*/ */
@ -906,6 +622,10 @@ class Form extends RequestHandler
*/ */
public function actionIsValidationExempt($action) public function actionIsValidationExempt($action)
{ {
// Non-actions don't bypass validation
if (!$action) {
return false;
}
if ($action->getValidationExempt()) { if ($action->getValidationExempt()) {
return true; return true;
} }
@ -1350,11 +1070,10 @@ class Form extends RequestHandler
{ {
if ($this->formActionPath) { if ($this->formActionPath) {
return $this->formActionPath; return $this->formActionPath;
} elseif ($this->controller->hasMethod("FormObjectLink")) {
return $this->controller->FormObjectLink($this->name);
} else {
return Controller::join_links($this->controller->Link(), $this->name);
} }
// Get action from request handler link
return $this->getRequestHandler()->Link();
} }
/** /**
@ -1406,9 +1125,9 @@ class Form extends RequestHandler
} }
/** /**
* Get the controller. * Get the controller or parent request handler.
* *
* @return Controller * @return RequestHandler
*/ */
public function getController() public function getController()
{ {
@ -1416,15 +1135,14 @@ class Form extends RequestHandler
} }
/** /**
* Set the controller. * Set the controller or parent request handler.
* *
* @param Controller $controller * @param RequestHandler $controller
* @return Form * @return $this
*/ */
public function setController($controller) public function setController(RequestHandler $controller = null)
{ {
$this->controller = $controller; $this->controller = $controller;
return $this; return $this;
} }
@ -1537,21 +1255,19 @@ class Form extends RequestHandler
*/ */
public function validationResult() public function validationResult()
{ {
// Opportunity to invalidate via validator // Automatically pass if there is no validator, or the clicked button is exempt
$action = $this->buttonClicked(); // Note: Soft support here for validation with absent request handler
if ($action && $this->actionIsValidationExempt($action)) { $handler = $this->getRequestHandler();
$action = $handler ? $handler->buttonClicked() : null;
$validator = $this->getValidator();
if (!$validator || $this->actionIsValidationExempt($action)) {
return ValidationResult::create(); return ValidationResult::create();
} }
// Invoke validator // Invoke validator
if ($this->validator) { $result = $validator->validate();
$result = $this->validator->validate(); $this->loadMessagesFrom($result);
$this->loadMessagesFrom($result); return $result;
return $result;
}
// Successful result
return ValidationResult::create();
} }
const MERGE_DEFAULT = 0; const MERGE_DEFAULT = 0;
@ -1765,6 +1481,7 @@ class Form extends RequestHandler
$data = array(); $data = array();
if ($dataFields) { if ($dataFields) {
/** @var FormField $field */
foreach ($dataFields as $field) { foreach ($dataFields as $field) {
if ($field->getName()) { if ($field->getName()) {
$data[$field->getName()] = $field->dataValue(); $data[$field->getName()] = $field->dataValue();
@ -1830,7 +1547,7 @@ class Form extends RequestHandler
$content = $this->forTemplate(); $content = $this->forTemplate();
$this->IncludeFormTag = true; $this->IncludeFormTag = true;
$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\"" $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName() . "_form_action\""
. " value=\"" . $this->FormAction() . "\" />\n"; . " value=\"" . $this->FormAction() . "\" />\n";
$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n"; $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n"; $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
@ -1858,53 +1575,6 @@ class Form extends RequestHandler
return $template->process($custom); return $template->process($custom);
} }
/**
* Sets the button that was clicked. This should only be called by the Controller.
*
* @param callable $funcName The name of the action method that will be called.
* @return $this
*/
public function setButtonClicked($funcName)
{
$this->buttonClickedFunc = $funcName;
return $this;
}
/**
* @return FormAction
*/
public function buttonClicked()
{
$actions = $this->getAllActions();
foreach ($actions as $action) {
if ($this->buttonClickedFunc === $action->actionName()) {
return $action;
}
}
return null;
}
/**
* Get a list of all actions, including those in the main "fields" FieldList
*
* @return array
*/
protected function getAllActions()
{
$fields = $this->fields->dataFields() ?: array();
$actions = $this->actions->dataFields() ?: array();
$fieldsAndActions = array_merge($fields, $actions);
$actions = array_filter($fieldsAndActions, function ($fieldOrAction) {
return $fieldOrAction instanceof FormAction;
});
return $actions;
}
/** /**
* Return the default button that should be clicked when another one isn't * Return the default button that should be clicked when another one isn't
* available. * available.
@ -2044,37 +1714,46 @@ class Form extends RequestHandler
return $result; return $result;
} }
/**
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// * Current request handler, build by buildRequestHandler(),
// TESTING HELPERS * accessed by getRequestHandler()
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// *
* @var FormRequestHandler
*/
protected $requestHandler = null;
/** /**
* Test a submission of this form. * Get request handler for this form
* @param string $action *
* @param array $data * @return FormRequestHandler
* @return HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test.
* @throws HTTPResponse_Exception
*/ */
public function testSubmission($action, $data) public function getRequestHandler()
{ {
$data['action_' . $action] = true; if (!$this->requestHandler) {
$this->requestHandler = $this->buildRequestHandler();
return Director::test($this->FormAction(), $data, Controller::curr()->getSession()); }
return $this->requestHandler;
} }
/** /**
* Test an ajax submission of this form. * Assign a specific request handler for this form
* *
* @param string $action * @param FormRequestHandler $handler
* @param array $data * @return $this
* @return HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test.
*/ */
public function testAjaxSubmission($action, $data) public function setRequestHandler(FormRequestHandler $handler)
{ {
$data['ajax'] = 1; $this->requestHandler = $handler;
return $this->testSubmission($action, $data); return $this;
}
/**
* Scaffold new request handler for this form
*
* @return FormRequestHandler
*/
protected function buildRequestHandler()
{
return FormRequestHandler::create($this);
} }
} }

View File

@ -0,0 +1,510 @@
<?php
namespace SilverStripe\Forms;
use BadMethodCallException;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException;
class FormRequestHandler extends RequestHandler
{
/**
* @var callable|null
*/
protected $buttonClickedFunc;
/**
* @config
* @var array
*/
private static $allowed_actions = array(
'handleField',
'httpSubmission',
'forTemplate',
);
/**
* @config
* @var array
*/
private static $url_handlers = array(
'field/$FieldName!' => 'handleField',
'POST ' => 'httpSubmission',
'GET ' => 'httpSubmission',
'HEAD ' => 'httpSubmission',
);
/**
* Form model being handled
*
* @var Form
*/
protected $form = null;
/**
* Build a new request handler for a given Form model
*
* @param Form $form
*/
public function __construct(Form $form)
{
$this->form = $form;
parent::__construct();
// Inherit parent controller request
$parent = $this->form->getController();
if ($parent) {
$this->setRequest($parent->getRequest());
}
}
/**
* Get link for this form
*
* @param string $action
* @return string
*/
public function Link($action = null)
{
// Forms without parent controller have no link;
// E.g. Submission handled via graphql
$controller = $this->form->getController();
if (empty($controller)) {
return null;
}
// Respect FormObjectLink() method
if ($controller->hasMethod("FormObjectLink")) {
return Controller::join_links(
$controller->FormObjectLink($this->form->getName()),
$action,
'/'
);
}
// Default form link
return Controller::join_links($controller->Link(), $this->form->getName(), $action, '/');
}
/**
* Handle a form submission. GET and POST requests behave identically.
* Populates the form with {@link loadDataFrom()}, calls {@link validate()},
* and only triggers the requested form action/method
* if the form is valid.
*
* @param HTTPRequest $request
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function httpSubmission($request)
{
// Strict method check
if ($this->form->getStrictFormMethodCheck()) {
// Throws an error if the method is bad...
$allowedMethod = $this->form->FormMethod();
if ($allowedMethod !== $request->httpMethod()) {
$response = Controller::curr()->getResponse();
$response->addHeader('Allow', $allowedMethod);
$this->httpError(405, _t(
"Form.BAD_METHOD",
"This form requires a {method} submission",
['method' => $allowedMethod]
));
}
// ...and only uses the variables corresponding to that method type
$vars = $allowedMethod === 'GET'
? $request->getVars()
: $request->postVars();
} else {
$vars = $request->requestVars();
}
// Ensure we only process saveable fields (non structural, readonly, or disabled)
$allowedFields = array_keys($this->form->Fields()->saveableFields());
// Populate the form
$this->form->loadDataFrom($vars, true, $allowedFields);
// Protection against CSRF attacks
// @todo Move this to SecurityTokenField::validate()
$token = $this->form->getSecurityToken();
if (! $token->checkRequest($request)) {
$securityID = $token->getName();
if (empty($vars[$securityID])) {
$this->httpError(400, _t(
"Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button, ".
"refresh your browser, and try again."
));
} else {
// Clear invalid token on refresh
$this->form->clearFormState();
$data = $this->form->getData();
unset($data[$securityID]);
$this->form
->setSessionData($data)
->sessionError(_t(
"Form.CSRF_EXPIRED_MESSAGE",
"Your session has expired. Please re-submit the form."
));
// Return the user
return $this->redirectBack();
}
}
// Determine the action button clicked
$funcName = null;
foreach ($vars as $paramName => $paramVal) {
if (substr($paramName, 0, 7) == 'action_') {
// Break off querystring arguments included in the action
if (strpos($paramName, '?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array();
parse_str($paramVars, $newRequestParams);
$vars = array_merge((array)$vars, (array)$newRequestParams);
}
// Cleanup action_, _x and _y from image fields
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
break;
}
}
// If the action wasn't set, choose the default on the form.
if (!isset($funcName) && $defaultAction = $this->form->defaultAction()) {
$funcName = $defaultAction->actionName();
}
if (isset($funcName)) {
$this->setButtonClicked($funcName);
}
// Permission checks (first on controller, then falling back to request handler)
$controller = $this->form->getController();
if (// Ensure that the action is actually a button or method on the form,
// and not just a method on the controller.
$controller
&& $controller->hasMethod($funcName)
&& !$controller->checkAccessAction($funcName)
// If a button exists, allow it on the controller
// buttonClicked() validates that the action set above is valid
&& !$this->buttonClicked()
) {
return $this->httpError(
403,
sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($controller))
);
} elseif (// No checks for button existence or $allowed_actions is performed -
// all form methods are callable (e.g. the legacy "callfieldmethod()")
$this->hasMethod($funcName)
&& !$this->checkAccessAction($funcName)
) {
return $this->httpError(
403,
sprintf('Action "%s" not allowed on form request handler (Class: "%s")', $funcName, get_class($this))
);
}
// Action handlers may throw ValidationExceptions.
try {
// Or we can use the Valiator attached to the form
$result = $this->form->validationResult();
if (!$result->isValid()) {
return $this->getValidationErrorResponse($result);
}
// First, try a handler method on the controller (has been checked for allowed_actions above already)
$controller = $this->form->getController();
if ($controller && $controller->hasMethod($funcName)) {
return $controller->$funcName($vars, $this->form, $request);
}
// Otherwise, try a handler method on the form request handler.
if ($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this->form, $request);
}
// Check for inline actions
$field = $this->checkFieldsForAction($this->form->Fields(), $funcName);
if ($field) {
return $field->$funcName($vars, $this->form, $request);
}
} catch (ValidationException $e) {
// The ValdiationResult contains all the relevant metadata
$result = $e->getResult();
$this->form->loadMessagesFrom($result);
return $this->getValidationErrorResponse($result);
}
// Determine if legacy form->allowed_actions is set
$legacyActions = $this->form->config()->get('allowed_actions');
if ($legacyActions) {
throw new BadMethodCallException(
"allowed_actions are not valid on Form class " . get_class($this->form) .
". Implement these in subclasses of " . get_class($this) . " instead"
);
}
return $this->httpError(404);
}
/**
* @param string $action
* @return bool
*/
public function checkAccessAction($action)
{
if (parent::checkAccessAction($action)) {
return true;
}
$actions = $this->getAllActions();
foreach ($actions as $formAction) {
if ($formAction->actionName() === $action) {
return true;
}
}
// Always allow actions on fields
$field = $this->checkFieldsForAction($this->form->Fields(), $action);
if ($field && $field->checkAccessAction($action)) {
return true;
}
return false;
}
/**
* Returns the appropriate response up the controller chain
* if {@link validate()} fails (which is checked prior to executing any form actions).
* By default, returns different views for ajax/non-ajax request, and
* handles 'application/json' requests with a JSON object containing the error messages.
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
* and can be overruled by setting {@link $validationResponseCallback}.
*
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getValidationErrorResponse(ValidationResult $result)
{
// Check for custom handling mechanism
$callback = $this->form->getValidationResponseCallback();
if ($callback && $callbackResponse = call_user_func($callback, $result)) {
return $callbackResponse;
}
// Check if handling via ajax
if ($this->getRequest()->isAjax()) {
return $this->getAjaxErrorResponse($result);
}
// Prior to redirection, persist this result in session to re-display on redirect
$this->form->setSessionValidationResult($result);
$this->form->setSessionData($this->form->getData());
// Determine redirection method
if ($this->form->getRedirectToFormOnValidationError()) {
return $this->redirectBackToForm();
}
return $this->redirectBack();
}
/**
* Redirect back to this form with an added #anchor link
*
* @return HTTPResponse
*/
public function redirectBackToForm()
{
$pageURL = $this->getReturnReferer();
if (!$pageURL) {
return $this->redirectBack();
}
// Add backURL and anchor
$pageURL = Controller::join_links(
$this->addBackURLParam($pageURL),
'#' . $this->form->FormName()
);
// Redirect
return $this->redirect($pageURL);
}
/**
* Helper to add ?BackURL= to any link
*
* @param string $link
* @return string
*/
protected function addBackURLParam($link)
{
$backURL = $this->getBackURL();
if ($backURL) {
return Controller::join_links($link, '?BackURL=' . urlencode($backURL));
}
return $link;
}
/**
* Build HTTP error response for ajax requests
*
* @internal called from {@see Form::getValidationErrorResponse}
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getAjaxErrorResponse(ValidationResult $result)
{
// Ajax form submissions accept json encoded errors by default
$acceptType = $this->getRequest()->getHeader('Accept');
if (strpos($acceptType, 'application/json') !== false) {
// Send validation errors back as JSON with a flag at the start
$response = new HTTPResponse(Convert::array2json($result->getMessages()));
$response->addHeader('Content-Type', 'application/json');
return $response;
}
// Send the newly rendered form tag as HTML
$this->form->loadMessagesFrom($result);
$response = new HTTPResponse($this->form->forTemplate());
$response->addHeader('Content-Type', 'text/html');
return $response;
}
/**
* Fields can have action to, let's check if anyone of the responds to $funcname them
*
* @param SS_List|array $fields
* @param callable $funcName
* @return FormField
*/
protected function checkFieldsForAction($fields, $funcName)
{
foreach ($fields as $field) {
/** @skipUpgrade */
if (ClassInfo::hasMethod($field, 'FieldList')) {
if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
return $field;
}
} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
return $field;
}
}
return null;
}
/**
* Handle a field request.
* Uses {@link Form->dataFieldByName()} to find a matching field,
* and falls back to {@link FieldList->fieldByName()} to look
* for tabs instead. This means that if you have a tab and a
* formfield with the same name, this method gives priority
* to the formfield.
*
* @param HTTPRequest $request
* @return FormField
*/
public function handleField($request)
{
$field = $this->form->Fields()->dataFieldByName($request->param('FieldName'));
if ($field) {
return $field;
} else {
// falling back to fieldByName, e.g. for getting tabs
return $this->form->Fields()->fieldByName($request->param('FieldName'));
}
}
/**
* Sets the button that was clicked. This should only be called by the Controller.
*
* @param callable $funcName The name of the action method that will be called.
* @return $this
*/
public function setButtonClicked($funcName)
{
$this->buttonClickedFunc = $funcName;
return $this;
}
/**
* Get instance of button which was clicked for this request
*
* @return FormAction
*/
public function buttonClicked()
{
$actions = $this->getAllActions();
foreach ($actions as $action) {
if ($this->buttonClickedFunc === $action->actionName()) {
return $action;
}
}
return null;
}
/**
* Get a list of all actions, including those in the main "fields" FieldList
*
* @return array
*/
protected function getAllActions()
{
$fields = $this->form->Fields()->dataFields();
$actions = $this->form->Actions()->dataFields();
$fieldsAndActions = array_merge($fields, $actions);
$actions = array_filter($fieldsAndActions, function ($fieldOrAction) {
return $fieldOrAction instanceof FormAction;
});
return $actions;
}
/**
* Processing that occurs before a form is executed.
*
* This includes form validation, if it fails, we throw a ValidationException
*
* This includes form validation, if it fails, we redirect back
* to the form with appropriate error messages.
* Always return true if the current form action is exempt from validation
*
* Triggered through {@link httpSubmission()}.
*
*
* Note that CSRF protection takes place in {@link httpSubmission()},
* if it fails the form data will never reach this method.
*
* @return ValidationResult
*/
public function validationResult()
{
// Check if button is exempt, or if there is no validator
$action = $this->buttonClicked();
$validator = $this->form->getValidator();
if (!$validator || $this->form->actionIsValidationExempt($action)) {
return ValidationResult::create();
}
// Invoke validator
$result = $validator->validate();
$this->form->loadMessagesFrom($result);
return $result;
}
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Forms\GridField; namespace SilverStripe\Forms\GridField;
use SilverStripe\Control\HasRequestHandler;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
@ -1032,7 +1033,13 @@ class GridField extends FormField
return $result; return $result;
} }
if ($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) { if ($this !== $result &&
!$request->isEmptyPattern($rule) &&
($result instanceof RequestHandler || $result instanceof HasRequestHandler)
) {
if ($result instanceof HasRequestHandler) {
$result = $result->getRequestHandler();
}
$returnValue = $result->handleRequest($request, $model); $returnValue = $result->handleRequest($request, $model);
if (is_array($returnValue)) { if (is_array($returnValue)) {

View File

@ -3,13 +3,13 @@
namespace SilverStripe\Forms\GridField; namespace SilverStripe\Forms\GridField;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Extensible; use SilverStripe\Core\Extensible;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Object; use SilverStripe\Core\Object;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\Validator; use SilverStripe\Forms\Validator;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use Closure; use Closure;
@ -128,7 +128,7 @@ class GridFieldDetailForm implements GridField_URLHandler
* *
* @param GridField $gridField * @param GridField $gridField
* @param DataObject $record * @param DataObject $record
* @param Controller $requestHandler * @param RequestHandler $requestHandler
* @return GridFieldDetailForm_ItemRequest * @return GridFieldDetailForm_ItemRequest
*/ */
protected function getItemRequestHandler($gridField, $record, $requestHandler) protected function getItemRequestHandler($gridField, $record, $requestHandler)

View File

@ -212,11 +212,6 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
} }
} }
// Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead.
// Thanks to this however, we are able to nest GridFields, and also access the initial Controller by
// dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController
// below.
$form = new Form( $form = new Form(
$this, $this,
'ItemEditForm', 'ItemEditForm',

View File

@ -18,9 +18,6 @@ use SilverStripe\Forms\FormAction;
*/ */
class CMSMemberLoginForm extends LoginForm class CMSMemberLoginForm extends LoginForm
{ {
protected $authenticator_class = 'SilverStripe\\Security\\MemberAuthenticator';
/** /**
* Get link to use for external security actions * Get link to use for external security actions
* *
@ -29,7 +26,7 @@ class CMSMemberLoginForm extends LoginForm
*/ */
public function getExternalLink($action = null) public function getExternalLink($action = null)
{ {
return Security::create()->Link($action); return Security::singleton()->Link($action);
} }
public function __construct(Controller $controller, $name) public function __construct(Controller $controller, $name)
@ -78,110 +75,8 @@ class CMSMemberLoginForm extends LoginForm
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $name, $fields, $actions);
} }
/** protected function buildRequestHandler()
* Try to authenticate the user
*
* @param array $data Submitted data
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($data)
{ {
$authenticator = $this->authenticator_class; return CMSMemberLoginHandler::create($this);
/** @var Member $member */
$member = $authenticator::authenticate($data, $this);
if ($member) {
$member->logIn(isset($data['Remember']));
return $member;
}
$this->extend('authenticationFailed', $data);
return null;
}
/**
* Login form handler method
*
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
* @return HTTPResponse
*/
public function dologin($data)
{
if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
} else {
// Find best url to redirect back to
$request = $this->controller->getRequest();
$url = $request->getHeader('X-Backurl')
?: $request->getHeader('Referer')
?: $this->controller->Link('login');
return $this->controller->redirect($url);
}
}
/**
* Redirect the user to the change password form.
*
* @skipUpgrade
* @return HTTPResponse
*/
protected function redirectToChangePassword()
{
// Since this form is loaded via an iframe, this redirect must be performed via javascript
$changePasswordForm = new ChangePasswordForm($this->controller, 'ChangePasswordForm');
$changePasswordForm->sessionMessage(
_t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
// Get redirect url
$changePasswordURL = $this->getExternalLink('changepassword');
if ($backURL = $this->controller->getRequest()->requestVar('BackURL')) {
Session::set('BackURL', $backURL);
$changePasswordURL = Controller::join_links($changePasswordURL, '?BackURL=' . urlencode($backURL));
}
$changePasswordURLATT = Convert::raw2att($changePasswordURL);
$changePasswordURLJS = Convert::raw2js($changePasswordURL);
$message = _t(
'CMSMemberLoginForm.PASSWORDEXPIRED',
'<p>Your password has expired. <a target="_top" href="{link}">Please choose a new one.</a></p>',
'Message displayed to user if their session cannot be restored',
array('link' => $changePasswordURLATT)
);
// Redirect to change password page
$this->controller->getResponse()->setStatusCode(200);
$this->controller->getResponse()->setBody(<<<PHP
<!DOCTYPE html>
<html><body>
$message
<script type="application/javascript">
setTimeout(function(){top.location.href = "$changePasswordURLJS";}, 0);
</script>
</body></html>
PHP
);
return $this->controller->getResponse();
}
/**
* Send user to the right location after login
*
* @param array $data
* @return HTTPResponse
*/
protected function logInUserAndRedirect($data)
{
// Check password expiry
if (Member::currentUser()->isPasswordExpired()) {
// Redirect the user to the external password change form if necessary
return $this->redirectToChangePassword();
} else {
// Link to success template
$url = $this->controller->Link('success');
return $this->controller->redirect($url);
}
} }
} }

View File

@ -0,0 +1,93 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
class CMSMemberLoginHandler extends MemberLoginHandler
{
/**
* Login form handler method
*
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
* @return HTTPResponse
*/
public function dologin($data)
{
if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
}
return $this->redirectBackToForm();
}
public function redirectBackToForm()
{
// Redirect back to form
$url = $this->addBackURLParam(CMSSecurity::singleton()->Link('login'));
return $this->redirect($url);
}
/**
* Redirect the user to the change password form.
*
* @skipUpgrade
* @return HTTPResponse
*/
protected function redirectToChangePassword()
{
// Since this form is loaded via an iframe, this redirect must be performed via javascript
$changePasswordForm = ChangePasswordForm::create($this->form->getController(), 'ChangePasswordForm');
$changePasswordForm->sessionMessage(
_t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
// Get redirect url
$changePasswordURL = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
$changePasswordURLATT = Convert::raw2att($changePasswordURL);
$changePasswordURLJS = Convert::raw2js($changePasswordURL);
$message = _t(
'CMSMemberLoginForm.PASSWORDEXPIRED',
'<p>Your password has expired. <a target="_top" href="{link}">Please choose a new one.</a></p>',
'Message displayed to user if their session cannot be restored',
array('link' => $changePasswordURLATT)
);
// Redirect to change password page
$response = HTTPResponse::create()
->setBody(<<<PHP
<!DOCTYPE html>
<html><body>
$message
<script type="application/javascript">
setTimeout(function(){top.location.href = "$changePasswordURLJS";}, 0);
</script>
</body></html>
PHP
);
return $response;
}
/**
* Send user to the right location after login
*
* @param array $data
* @return HTTPResponse
*/
protected function logInUserAndRedirect($data)
{
// Check password expiry
if (Member::currentUser()->isPasswordExpired()) {
// Redirect the user to the external password change form if necessary
return $this->redirectToChangePassword();
}
// Link to success template
$url = CMSSecurity::singleton()->Link('success');
return $this->redirect($url);
}
}

View File

@ -2,30 +2,24 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Director; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\HTTP;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\Forms\PasswordField; use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationResult;
/** /**
* Standard Change Password Form * Standard Change Password Form
*/ */
class ChangePasswordForm extends Form class ChangePasswordForm extends Form
{ {
/** /**
* Constructor * Constructor
* *
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param RequestHandler $controller The parent controller, necessary to create the appropriate form action tag.
* @param string $name The method on the controller that will return this form object. * @param string $name The method on the controller that will return this form object.
* @param FieldList|FormField $fields All of the fields in the form - a {@link FieldList} of * @param FieldList|FormField $fields All of the fields in the form - a {@link FieldList} of
* {@link FormField} objects. * {@link FormField} objects.
@ -33,11 +27,7 @@ class ChangePasswordForm extends Form
*/ */
public function __construct($controller, $name, $fields = null, $actions = null) public function __construct($controller, $name, $fields = null, $actions = null)
{ {
if (isset($_REQUEST['BackURL'])) { $backURL = $controller->getBackURL() ?: Session::get('BackURL');
$backURL = $_REQUEST['BackURL'];
} else {
$backURL = Session::get('BackURL');
}
if (!$fields) { if (!$fields) {
$fields = new FieldList(); $fields = new FieldList();
@ -58,102 +48,18 @@ class ChangePasswordForm extends Form
); );
} }
if (isset($backURL)) { if ($backURL) {
$fields->push(new HiddenField('BackURL', 'BackURL', $backURL)); $fields->push(new HiddenField('BackURL', false, $backURL));
} }
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $name, $fields, $actions);
} }
/** /**
* Change the password * @return ChangePasswordHandler
*
* @param array $data The user submitted data
* @return HTTPResponse
*/ */
public function doChangePassword(array $data) protected function buildRequestHandler()
{ {
if ($member = Member::currentUser()) { return ChangePasswordHandler::create($this);
// The user was logged in, check the current password
if (empty($data['OldPassword']) || !$member->checkPassword($data['OldPassword'])->isValid()) {
$this->clearMessage();
$this->sessionMessage(
_t('Member.ERRORPASSWORDNOTMATCH', "Your current password does not match, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
}
if (!$member) {
if (Session::get('AutoLoginHash')) {
$member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
}
// The user is not logged in and no valid auto login hash is available
if (!$member) {
Session::clear('AutoLoginHash');
return $this->controller->redirect($this->controller->Link('login'));
}
}
// Check the new password
if (empty($data['NewPassword1'])) {
$this->clearMessage();
$this->sessionMessage(
_t('Member.EMPTYNEWPASSWORD', "The new password can't be empty, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Fail if passwords do not match
if ($data['NewPassword1'] !== $data['NewPassword2']) {
$this->clearMessage();
$this->sessionMessage(
_t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$this->setSessionValidationResult($validationResult);
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if ($member->canLogIn()->isValid()) {
$member->logIn();
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
if (!empty($_REQUEST['BackURL'])
// absolute redirection URLs may cause spoofing
&& Director::is_site_url($_REQUEST['BackURL'])
) {
$url = Director::absoluteURL($_REQUEST['BackURL']);
return $this->controller->redirect($url);
} else {
// Redirect to default location - the login form saying "You are logged in as..."
$redirectURL = HTTP::setGetVar(
'BackURL',
Director::absoluteBaseURL(),
$this->controller->Link('login')
);
return $this->controller->redirect($redirectURL);
}
} }
} }

View File

@ -0,0 +1,103 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FormRequestHandler;
class ChangePasswordHandler extends FormRequestHandler
{
/**
* Change the password
*
* @param array $data The user submitted data
* @return HTTPResponse
*/
public function doChangePassword(array $data)
{
$member = Member::currentUser();
// The user was logged in, check the current password
if ($member && (
empty($data['OldPassword']) ||
!$member->checkPassword($data['OldPassword'])->isValid()
)) {
$this->form->sessionMessage(
_t('Member.ERRORPASSWORDNOTMATCH', "Your current password does not match, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
if (!$member) {
if (Session::get('AutoLoginHash')) {
$member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
}
// The user is not logged in and no valid auto login hash is available
if (!$member) {
Session::clear('AutoLoginHash');
return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
}
}
// Check the new password
if (empty($data['NewPassword1'])) {
$this->form->sessionMessage(
_t('Member.EMPTYNEWPASSWORD', "The new password can't be empty, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Fail if passwords do not match
if ($data['NewPassword1'] !== $data['NewPassword2']) {
$this->form->sessionMessage(
_t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$this->form->setSessionValidationResult($validationResult);
return $this->redirectBackToForm();
}
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if ($member->canLogIn()->isValid()) {
$member->logIn();
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
// Redirect to backurl
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// Redirect to default location - the login form saying "You are logged in as..."
$url = Security::singleton()->Link('login');
return $this->redirect($url);
}
public function redirectBackToForm()
{
// Redirect back to form
$url = $this->addBackURLParam(CMSSecurity::singleton()->Link('changepassword'));
return $this->redirect($url);
}
}

View File

@ -2,13 +2,9 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email;
use SilverStripe\Dev\Debug;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -39,17 +35,6 @@ class MemberLoginForm extends LoginForm
*/ */
public $loggedInAsField = 'FirstName'; public $loggedInAsField = 'FirstName';
protected $authenticator_class = 'SilverStripe\\Security\\MemberAuthenticator';
/**
* Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
* remain valid actions regardless of the member login state.
*
* @var array
* @config
*/
private static $allowed_actions = array('dologin', 'logout');
/** /**
* Constructor * Constructor
* *
@ -184,205 +169,11 @@ JS;
return $this; return $this;
} }
/** /**
* Login form handler method * @return MemberLoginHandler
*
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
*/ */
public function dologin($data) protected function buildRequestHandler()
{ {
if ($this->performLogin($data)) { return MemberLoginHandler::create($this);
$this->logInUserAndRedirect($data);
} else {
/** @skipUpgrade */
if (array_key_exists('Email', $data)) {
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember']));
}
if (isset($_REQUEST['BackURL'])) {
$backURL = $_REQUEST['BackURL'];
} else {
$backURL = null;
}
if ($backURL) {
Session::set('BackURL', $backURL);
}
// Show the right tab on failed login
$loginLink = Director::absoluteURL($this->controller->Link('login'));
if ($backURL) {
$loginLink .= '?BackURL=' . urlencode($backURL);
}
$this->controller->redirect($loginLink . '#' . $this->FormName() .'_tab');
}
}
/**
* Login in the user and figure out where to redirect the browser.
*
* The $data has this format
* array(
* 'AuthenticationMethod' => 'MemberAuthenticator',
* 'Email' => 'sam@silverstripe.com',
* 'Password' => '1nitialPassword',
* 'BackURL' => 'test/link',
* [Optional: 'Remember' => 1 ]
* )
*
* @param array $data
* @return HTTPResponse
*/
protected function logInUserAndRedirect($data)
{
Session::clear('SessionForms.MemberLoginForm.Email');
Session::clear('SessionForms.MemberLoginForm.Remember');
if (Member::currentUser()->isPasswordExpired()) {
if (isset($_REQUEST['BackURL']) && $backURL = $_REQUEST['BackURL']) {
Session::set('BackURL', $backURL);
}
/** @skipUpgrade */
$cp = ChangePasswordForm::create($this->controller, 'ChangePasswordForm');
$cp->sessionMessage(
_t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
return $this->controller->redirect('Security/changepassword');
}
// Absolute redirection URLs may cause spoofing
if (!empty($_REQUEST['BackURL'])) {
$url = $_REQUEST['BackURL'];
if (Director::is_site_url($url)) {
$url = Director::absoluteURL($url);
} else {
// Spoofing attack, redirect to homepage instead of spoofing url
$url = Director::absoluteBaseURL();
}
return $this->controller->redirect($url);
}
// If a default login dest has been set, redirect to that.
if ($url = Security::config()->default_login_dest) {
$url = Controller::join_links(Director::absoluteBaseURL(), $url);
return $this->controller->redirect($url);
}
// Redirect the user to the page where they came from
$member = Member::currentUser();
if ($member) {
$firstname = Convert::raw2xml($member->FirstName);
if (!empty($data['Remember'])) {
Session::set('SessionForms.MemberLoginForm.Remember', '1');
$member->logIn(true);
} else {
$member->logIn();
}
$message = _t('Member.WELCOMEBACK', "Welcome Back, {firstname}", array('firstname' => $firstname));
Security::setLoginMessage($message, ValidationResult::TYPE_GOOD);
}
return Controller::curr()->redirectBack();
}
/**
* Log out form handler method
*
* This method is called when the user clicks on "logout" on the form
* created when the parameter <i>$checkCurrentUser</i> of the
* {@link __construct constructor} was set to TRUE and the user was
* currently logged in.
*/
public function logout()
{
$s = new Security();
$s->logout();
}
/**
* Try to authenticate the user
*
* @param array $data Submitted data
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($data)
{
$member = call_user_func_array(array($this->authenticator_class, 'authenticate'), array($data, $this));
if ($member) {
$member->LogIn(isset($data['Remember']));
return $member;
} else {
$this->extend('authenticationFailed', $data);
return null;
}
}
/**
* 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.
*
* @skipUpgrade
* @param array $data Submitted data
* @return HTTPResponse
*/
public function forgotPassword($data)
{
// Ensure password is given
if (empty($data['Email'])) {
$this->sessionMessage(
_t('Member.ENTEREMAIL', 'Please enter an email address to get a password reset link.'),
'bad'
);
return $this->controller->redirect('Security/lostpassword');
}
// Find existing member
/** @var Member $member */
$member = Member::get()->filter("Email", $data['Email'])->first();
// 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();
Email::create()
->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
->setData($member)
->setSubject(_t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject'))
->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token))
->setTo($member->Email)
->send();
return $this->controller->redirect('Security/passwordsent/' . urlencode($data['Email']));
} elseif ($data['Email']) {
// Avoid information disclosure by displaying the same status,
// regardless wether the email address actually exists
return $this->controller->redirect('Security/passwordsent/' . rawurlencode($data['Email']));
} else {
$this->sessionMessage(
_t('Member.ENTEREMAIL', 'Please enter an email address to get a password reset link.'),
'bad'
);
return $this->controller->redirect('Security/lostpassword');
}
} }
} }

View File

@ -0,0 +1,240 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FormRequestHandler;
use SilverStripe\ORM\ValidationResult;
/**
* Handle login requests from MemberLoginForm
*/
class MemberLoginHandler extends FormRequestHandler
{
protected $authenticator_class = MemberAuthenticator::class;
/**
* Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
* remain valid actions regardless of the member login state.
*
* @var array
* @config
*/
private static $allowed_actions = [
'dologin',
'logout',
];
/**
* Login form handler method
*
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
* @return HTTPResponse
*/
public function dologin($data)
{
if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
}
/** @skipUpgrade */
if (array_key_exists('Email', $data)) {
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember']));
}
// Fail to login redirects back to form
return $this->redirectBackToForm();
}
/**
* Redirect to password recovery form
*
* @return HTTPResponse
*/
public function redirectToLostPassword()
{
$lostPasswordLink = Security::singleton()->Link('lostpassword');
return $this->redirect($this->addBackURLParam($lostPasswordLink));
}
public function getReturnReferer()
{
// Home of login form is always this url
return Security::singleton()->Link('login');
}
/**
* Login in the user and figure out where to redirect the browser.
*
* The $data has this format
* array(
* 'AuthenticationMethod' => 'MemberAuthenticator',
* 'Email' => 'sam@silverstripe.com',
* 'Password' => '1nitialPassword',
* 'BackURL' => 'test/link',
* [Optional: 'Remember' => 1 ]
* )
*
* @param array $data
* @return HTTPResponse
*/
protected function logInUserAndRedirect($data)
{
Session::clear('SessionForms.MemberLoginForm.Email');
Session::clear('SessionForms.MemberLoginForm.Remember');
$member = Member::currentUser();
if ($member->isPasswordExpired()) {
return $this->redirectToChangePassword();
}
// Absolute redirection URLs may cause spoofing
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// If a default login dest has been set, redirect to that.
$defaultLoginDest = Security::config()->get('default_login_dest');
if ($defaultLoginDest) {
return $this->redirect($defaultLoginDest);
}
// Redirect the user to the page where they came from
if ($member) {
if (!empty($data['Remember'])) {
Session::set('SessionForms.MemberLoginForm.Remember', '1');
$member->logIn(true);
} else {
$member->logIn();
}
// Welcome message
$message = _t(
'Member.WELCOMEBACK',
"Welcome Back, {firstname}",
['firstname' => $member->FirstName]
);
Security::setLoginMessage($message, ValidationResult::TYPE_GOOD);
}
// Redirect back
return $this->redirectBack();
}
/**
* Log out form handler method
*
* This method is called when the user clicks on "logout" on the form
* created when the parameter <i>$checkCurrentUser</i> of the
* {@link __construct constructor} was set to TRUE and the user was
* currently logged in.
*
* @return HTTPResponse
*/
public function logout()
{
return Security::singleton()->logout();
}
/**
* Try to authenticate the user
*
* @param array $data Submitted data
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($data)
{
$member = call_user_func_array(
[$this->authenticator_class, 'authenticate'],
[$data, $this->form]
);
if ($member) {
$member->LogIn(isset($data['Remember']));
return $member;
}
// No member, can't login
$this->extend('authenticationFailed', $data);
return null;
}
/**
* 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.
*
* @skipUpgrade
* @param array $data Submitted data
* @return HTTPResponse
*/
public function forgotPassword($data)
{
// Ensure password is given
if (empty($data['Email'])) {
$this->form->sessionMessage(
_t('Member.ENTEREMAIL', 'Please enter an email address to get a password reset link.'),
'bad'
);
return $this->redirectToLostPassword();
}
// Find existing member
/** @var Member $member */
$member = Member::get()->filter("Email", $data['Email'])->first();
// Allow vetoing forgot password requests
$results = $this->extend('forgotPassword', $member);
if ($results && is_array($results) && in_array(false, $results, true)) {
return $this->redirectToLostPassword();
}
if ($member) {
$token = $member->generateAutologinTokenAndStoreHash();
Email::create()
->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
->setData($member)
->setSubject(_t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject'))
->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token))
->setTo($member->Email)
->send();
}
// Avoid information disclosure by displaying the same status,
// regardless wether the email address actually exists
$link = Controller::join_links(
Security::singleton()->Link('passwordsent'),
rawurlencode($data['Email']),
'/'
);
return $this->redirect($this->addBackURLParam($link));
}
/**
* Invoked if password is expired and must be changed
*
* @skipUpgrade
* @return HTTPResponse
*/
protected function redirectToChangePassword()
{
$cp = ChangePasswordForm::create($this->form->getController(), 'ChangePasswordForm');
$cp->sessionMessage(
_t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
$changedPasswordLink = Security::singleton()->Link('changepassword');
return $this->redirect($this->addBackURLParam($changedPasswordLink));
}
}

View File

@ -2,17 +2,15 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use Page;
use SilverStripe\CMS\Controllers\ContentController; use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\EmailField; use SilverStripe\Forms\EmailField;
@ -28,6 +26,7 @@ use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
use Exception; use Exception;
use SilverStripe\View\ViewableData_Customised;
use Subsite; use Subsite;
/** /**
@ -130,7 +129,7 @@ class Security extends Controller implements TemplateGlobalProvider
* @var string * @var string
* @config * @config
*/ */
private static $page_class = 'Page'; private static $page_class = Page::class;
/** /**
* Default message set used in permission failures. * Default message set used in permission failures.
@ -192,18 +191,6 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
private static $robots_tag = 'noindex, nofollow'; private static $robots_tag = 'noindex, nofollow';
/**
* Get location of word list file
*
* @deprecated 4.0 Use the "Security.word_list" config setting instead
*/
public static function get_word_list()
{
Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
return self::config()->word_list;
}
/** /**
* Enable or disable recording of login attempts * Enable or disable recording of login attempts
* through the {@link LoginRecord} object. * through the {@link LoginRecord} object.
@ -227,31 +214,6 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
static $database_is_ready = false; static $database_is_ready = false;
/**
* Set location of word list file
*
* @deprecated 4.0 Use the "Security.word_list" config setting instead
* @param string $wordListFile Location of word list file
*/
public static function set_word_list($wordListFile)
{
Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
self::config()->word_list = $wordListFile;
}
/**
* Set the default message set used in permissions failures.
*
* @deprecated 4.0 Use the "Security.default_message_set" config setting instead
* @param string|array $messageSet
*/
public static function set_default_message_set($messageSet)
{
Deprecation::notice('4.0', 'Use the "Security.default_message_set" config setting instead');
self::config()->default_message_set = $messageSet;
}
/** /**
* Register that we've had a permission failure trying to view the given page * Register that we've had a permission failure trying to view the given page
* *
@ -377,13 +339,15 @@ class Security extends Controller implements TemplateGlobalProvider
parent::init(); parent::init();
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
if ($this->config()->frame_options) { $frameOptions = $this->config()->get('frame_options');
$this->getResponse()->addHeader('X-Frame-Options', $this->config()->frame_options); if ($frameOptions) {
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
} }
// Prevent search engines from indexing the login page // Prevent search engines from indexing the login page
if ($this->config()->robots_tag) { $robotsTag = $this->config()->get('robots_tag');
$this->getResponse()->addHeader('X-Robots-Tag', $this->config()->robots_tag); if ($robotsTag) {
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
} }
} }
@ -473,6 +437,7 @@ class Security extends Controller implements TemplateGlobalProvider
* - If it's false, the code calling logout() is * - If it's false, the code calling logout() is
* responsible for sending the user where-ever * responsible for sending the user where-ever
* they should go. * they should go.
* @return HTTPResponse|null
*/ */
public function logout($redirect = true) public function logout($redirect = true)
{ {
@ -482,8 +447,9 @@ class Security extends Controller implements TemplateGlobalProvider
} }
if ($redirect && (!$this->getResponse()->isFinished())) { if ($redirect && (!$this->getResponse()->isFinished())) {
$this->redirectBack(); return $this->redirectBack();
} }
return null;
} }
/** /**
@ -534,21 +500,24 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
protected function getResponseController($title) protected function getResponseController($title)
{ {
if (!class_exists('SilverStripe\\CMS\\Model\\SiteTree')) { // Use the default setting for which Page to use to render the security page
$pageClass = $this->stat('page_class');
if (!$pageClass || !class_exists($pageClass)) {
return $this; return $this;
} }
// Use the default setting for which Page to use to render the security page // Create new instance of page holder
$pageClass = $this->stat('page_class'); /** @var Page $holderPage */
$tmpPage = new $pageClass; $holderPage = new $pageClass;
$tmpPage->Title = $title; $holderPage->Title = $title;
/** @skipUpgrade */ /** @skipUpgrade */
$tmpPage->URLSegment = 'Security'; $holderPage->URLSegment = 'Security';
// Disable ID-based caching of the log-in page by making it a random number // Disable ID-based caching of the log-in page by making it a random number
$tmpPage->ID = -1 * rand(1, 10000000); $holderPage->ID = -1 * rand(1, 10000000);
$controllerClass = $tmpPage->getControllerName(); $controllerClass = $holderPage->getControllerName();
$controller = $controllerClass::create($tmpPage); /** @var ContentController $controller */
$controller = $controllerClass::create($holderPage);
$controller->setDataModel($this->model); $controller->setDataModel($this->model);
$controller->doInit(); $controller->doInit();
return $controller; return $controller;
@ -698,19 +667,20 @@ class Security extends Controller implements TemplateGlobalProvider
return $response; return $response;
} }
$message = _t(
'Security.NOTERESETPASSWORD',
'Enter your e-mail address and we will send you a link with which you can reset your password'
);
/** @var ViewableData_Customised $customisedController */
$customisedController = $controller->customise(array( $customisedController = $controller->customise(array(
'Content' => 'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'<p>' .
_t(
'Security.NOTERESETPASSWORD',
'Enter your e-mail address and we will send you a link with which you can reset your password'
) .
'</p>',
'Form' => $this->LostPasswordForm(), 'Form' => $this->LostPasswordForm(),
)); ));
//Controller::$currentController = $controller; //Controller::$currentController = $controller;
return $customisedController->renderWith($this->getTemplatesFor('lostpassword')); $result = $customisedController->renderWith($this->getTemplatesFor('lostpassword'));
return $result;
} }
@ -757,21 +727,19 @@ class Security extends Controller implements TemplateGlobalProvider
$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension()); $email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
$message = _t(
'Security.PASSWORDSENTTEXT',
"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
. " address.",
array('email' => Convert::raw2xml($email))
);
$customisedController = $controller->customise(array( $customisedController = $controller->customise(array(
'Title' => _t( 'Title' => _t(
'Security.PASSWORDSENTHEADER', 'Security.PASSWORDSENTHEADER',
"Password reset link sent to '{email}'", "Password reset link sent to '{email}'",
array('email' => $email) array('email' => $email)
), ),
'Content' => 'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
"<p>"
. _t(
'Security.PASSWORDSENTTEXT',
"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
. " address.",
array('email' => $email)
)
. "</p>",
'Email' => $email 'Email' => $email
)); ));
@ -844,31 +812,37 @@ class Security extends Controller implements TemplateGlobalProvider
} elseif (Session::get('AutoLoginHash')) { } elseif (Session::get('AutoLoginHash')) {
// Subsequent request after the "first load with hash" (see previous if clause). // Subsequent request after the "first load with hash" (see previous if clause).
$customisedController = $controller->customise(array( $customisedController = $controller->customise(array(
'Content' => 'Content' => DBField::create_field(
'<p>' . 'HTMLFragment',
_t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '<p>' . _t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '</p>'
'</p>', ),
'Form' => $this->ChangePasswordForm(), 'Form' => $this->ChangePasswordForm(),
)); ));
} elseif (Member::currentUser()) { } elseif (Member::currentUser()) {
// Logged in user requested a password change form. // Logged in user requested a password change form.
$customisedController = $controller->customise(array( $customisedController = $controller->customise(array(
'Content' => '<p>' 'Content' => DBField::create_field(
. _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>', 'HTMLFragment',
'<p>' . _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>'
),
'Form' => $this->ChangePasswordForm())); 'Form' => $this->ChangePasswordForm()));
} else { } else {
// Show friendly message if it seems like the user arrived here via password reset feature. // Show friendly message if it seems like the user arrived here via password reset feature.
if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) { if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
$customisedController = $controller->customise( $customisedController = $controller->customise(
array('Content' => array('Content' => DBField::create_field(
'HTMLFragment',
_t( _t(
'Security.NOTERESETLINKINVALID', 'Security.NOTERESETLINKINVALID',
'<p>The password reset link is invalid or expired.</p>' '<p>The password reset link is invalid or expired.</p>'
. '<p>You can request a new one <a href="{link1}">here</a> or change your password after' . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
. ' you <a href="{link2}">logged in</a>.</p>', . ' you <a href="{link2}">logged in</a>.</p>',
array('link1' => $this->Link('lostpassword'), 'link2' => $this->Link('login')) [
'link1' => $this->Link('lostpassword'),
'link2' => $this->Link('login')
]
) )
) ))
); );
} else { } else {
return self::permissionFailure( return self::permissionFailure(
@ -884,16 +858,12 @@ class Security extends Controller implements TemplateGlobalProvider
/** /**
* Factory method for the lost password form * Factory method for the lost password form
* *
* @skipUpgrade
* @return ChangePasswordForm Returns the lost password form * @return ChangePasswordForm Returns the lost password form
*/ */
public function ChangePasswordForm() public function ChangePasswordForm()
{ {
/** @skipUpgrade */ return ChangePasswordForm::create($this, 'ChangePasswordForm');
$formName = 'ChangePasswordForm';
return Injector::inst()->createWithArgs(
'SilverStripe\\Security\\ChangePasswordForm',
[ $this, $formName]
);
} }
/** /**
@ -1092,7 +1062,7 @@ class Security extends Controller implements TemplateGlobalProvider
{ {
// Fall back to the default encryption algorithm // Fall back to the default encryption algorithm
if (!$algorithm) { if (!$algorithm) {
$algorithm = self::config()->password_encryption_algorithm; $algorithm = self::config()->get('password_encryption_algorithm');
} }
$e = PasswordEncryptor::create_for_algorithm($algorithm); $e = PasswordEncryptor::create_for_algorithm($algorithm);
@ -1221,7 +1191,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function login_url() public static function login_url()
{ {
return Controller::join_links(Director::baseURL(), self::config()->login_url); return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
} }
@ -1234,7 +1204,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function logout_url() public static function logout_url()
{ {
return Controller::join_links(Director::baseURL(), self::config()->logout_url); return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
} }
/** /**
@ -1246,7 +1216,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function lost_password_url() public static function lost_password_url()
{ {
return Controller::join_links(Director::baseURL(), self::config()->lost_password_url); return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
} }
/** /**

View File

@ -561,7 +561,7 @@ class ViewableData extends Object implements IteratorAggregate
* @return string * @return string
* @uses ClassInfo * @uses ClassInfo
*/ */
public function CSSClasses($stopAtClass = 'SilverStripe\\View\\ViewableData') public function CSSClasses($stopAtClass = self::class)
{ {
$classes = array(); $classes = array();
$classAncestry = array_reverse(ClassInfo::ancestry($this->class)); $classAncestry = array_reverse(ClassInfo::ancestry($this->class));

View File

@ -3,8 +3,6 @@
// Bootstrap for running SapphireTests // Bootstrap for running SapphireTests
// Connect to database // Connect to database
use SilverStripe\Control\Tests\FakeController;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
require_once __DIR__ . '/../../src/Core/Core.php'; require_once __DIR__ . '/../../src/Core/Core.php';

View File

@ -124,16 +124,6 @@ class ControllerTest extends FunctionalTest
'when called with an action in the URL' 'when called with an action in the URL'
); );
Config::modify()->merge(RequestHandler::class, 'require_allowed_actions', false);
$response = $this->get("UnsecuredController/index");
$this->assertEquals(
200,
$response->getStatusCode(),
'Access granted on index action without $allowed_actions on defining controller, ' .
'when called with an action in the URL, and explicitly allowed through config'
);
Config::modify()->merge(RequestHandler::class, 'require_allowed_actions', true);
$response = $this->get("UnsecuredController/method1"); $response = $this->get("UnsecuredController/method1");
$this->assertEquals( $this->assertEquals(
403, 403,
@ -142,16 +132,6 @@ class ControllerTest extends FunctionalTest
'when called without an action in the URL' 'when called without an action in the URL'
); );
Config::modify()->merge(RequestHandler::class, 'require_allowed_actions', false);
$response = $this->get("UnsecuredController/method1");
$this->assertEquals(
200,
$response->getStatusCode(),
'Access granted on action without $allowed_actions on defining controller, ' .
'when called without an action in the URL, and explicitly allowed through config'
);
Config::modify()->merge(RequestHandler::class, 'require_allowed_actions', true);
$response = $this->get("AccessBaseController/"); $response = $this->get("AccessBaseController/");
$this->assertEquals( $this->assertEquals(
200, 200,
@ -503,9 +483,9 @@ class ControllerTest extends FunctionalTest
array('Referer' => $externalAbsoluteUrl) array('Referer' => $externalAbsoluteUrl)
); );
$this->assertEquals( $this->assertEquals(
200, Director::absoluteBaseURL(),
$response->getStatusCode(), $response->getHeader('Location'),
"Doesn't redirect on external URLs" "Redirects back to home page on external url"
); );
} }
@ -522,8 +502,10 @@ class ControllerTest extends FunctionalTest
"Redirects on internal relative URLs" "Redirects on internal relative URLs"
); );
// BackURL is internal link
$internalAbsoluteUrl = Director::absoluteBaseURL() . '/some-url'; $internalAbsoluteUrl = Director::absoluteBaseURL() . '/some-url';
$response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($internalAbsoluteUrl)); $link = 'TestController/redirectbacktest?BackURL=' . urlencode($internalAbsoluteUrl);
$response = $this->get($link);
$this->assertEquals($internalAbsoluteUrl, $response->getHeader('Location')); $this->assertEquals($internalAbsoluteUrl, $response->getHeader('Location'));
$this->assertEquals( $this->assertEquals(
302, 302,
@ -531,12 +513,13 @@ class ControllerTest extends FunctionalTest
"Redirects on internal absolute URLs" "Redirects on internal absolute URLs"
); );
// Note that this test is affected by the prior ->get()
$externalAbsoluteUrl = 'http://myhost.com/some-url'; $externalAbsoluteUrl = 'http://myhost.com/some-url';
$response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($externalAbsoluteUrl)); $response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($externalAbsoluteUrl));
$this->assertEquals( $this->assertEquals(
200, Director::absoluteURL($link),
$response->getStatusCode(), $response->getHeader('Location'),
"Doesn't redirect on external URLs" "If BackURL Is external link, fall back to last url (Referer)"
); );
} }

View File

@ -9,6 +9,8 @@ class AccessBaseController extends Controller implements TestOnly
{ {
private static $allowed_actions = array(); private static $allowed_actions = array();
private static $url_segment = 'AccessBaseController';
private static $extensions = [ private static $extensions = [
AccessBaseControllerExtension::class AccessBaseControllerExtension::class
]; ];

View File

@ -2,11 +2,11 @@
namespace SilverStripe\Control\Tests\ControllerTest; namespace SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
class AccessSecuredController extends ControllerTest\AccessBaseController implements TestOnly class AccessSecuredController extends AccessBaseController implements TestOnly
{ {
private static $url_segment = 'AccessSecuredController';
private static $allowed_actions = array( private static $allowed_actions = array(
"method1", // denied because only defined in parent "method1", // denied because only defined in parent

View File

@ -2,11 +2,11 @@
namespace SilverStripe\Control\Tests\ControllerTest; namespace SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
class AccessWildcardSecuredController extends ControllerTest\AccessBaseController implements TestOnly class AccessWildcardSecuredController extends AccessBaseController implements TestOnly
{ {
private static $url_segment = 'AccessWildcardSecuredController';
private static $allowed_actions = array( private static $allowed_actions = array(
"*" => "ADMIN", // should throw exception "*" => "ADMIN", // should throw exception

View File

@ -7,6 +7,7 @@ use SilverStripe\Dev\TestOnly;
class ContainerController extends Controller implements TestOnly class ContainerController extends Controller implements TestOnly
{ {
private static $url_segment = 'ContainerController';
private static $allowed_actions = array( private static $allowed_actions = array(
'subcontroller', 'subcontroller',

View File

@ -7,6 +7,7 @@ use SilverStripe\Dev\TestOnly;
class HasAction extends Controller implements TestOnly class HasAction extends Controller implements TestOnly
{ {
private static $url_segment = 'HasAction';
private static $allowed_actions = array( private static $allowed_actions = array(
'allowed_action', 'allowed_action',

View File

@ -6,6 +6,7 @@ use SilverStripe\Dev\TestOnly;
class HasAction_Unsecured extends HasAction implements TestOnly class HasAction_Unsecured extends HasAction implements TestOnly
{ {
private static $url_segment = 'HasAction_Unsecured';
public function defined_action() public function defined_action()
{ {

View File

@ -2,11 +2,11 @@
namespace SilverStripe\Control\Tests\ControllerTest; namespace SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Control\Tests\ControllerTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
class IndexSecuredController extends ControllerTest\AccessBaseController implements TestOnly class IndexSecuredController extends AccessBaseController implements TestOnly
{ {
private static $url_segment = 'IndexSecuredController';
private static $allowed_actions = array( private static $allowed_actions = array(
"index" => "ADMIN", "index" => "ADMIN",

View File

@ -7,6 +7,7 @@ use SilverStripe\Dev\TestOnly;
class SubController extends Controller implements TestOnly class SubController extends Controller implements TestOnly
{ {
private static $url_segment = 'SubController';
private static $allowed_actions = array( private static $allowed_actions = array(
'subaction', 'subaction',

View File

@ -10,6 +10,8 @@ use SilverStripe\Dev\TestOnly;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
private static $url_segment = 'TestController';
public $Content = "default content"; public $Content = "default content";
private static $allowed_actions = array( private static $allowed_actions = array(

View File

@ -7,6 +7,7 @@ use SilverStripe\Dev\TestOnly;
class UnsecuredController extends Controller implements TestOnly class UnsecuredController extends Controller implements TestOnly
{ {
private static $url_segment = 'UnsecuredController';
// Not defined, allow access to all // Not defined, allow access to all
// static $allowed_actions = array(); // static $allowed_actions = array();

View File

@ -2,12 +2,17 @@
namespace SilverStripe\Control\Tests; namespace SilverStripe\Control\Tests;
use SilverStripe\Control\Cookie_Backend;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\Tests\DirectorTest\TestController; use SilverStripe\Control\Tests\DirectorTest\TestController;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\RequestProcessor; use SilverStripe\Control\RequestProcessor;
use SilverStripe\Security\Security;
/** /**
* @todo test Director::alternateBaseFolder() * @todo test Director::alternateBaseFolder()
@ -116,7 +121,7 @@ class DirectorTest extends SapphireTest
$rootURL = Director::protocolAndHost(); $rootURL = Director::protocolAndHost();
$_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/"; $_SERVER['REQUEST_URI'] = "$rootURL/mysite/sub-page/";
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/mysite/'); Director::config()->set('alternate_base_url', '/mysite/');
//test empty / local urls //test empty / local urls
foreach (array('', './', '.') as $url) { foreach (array('', './', '.') as $url) {
@ -176,7 +181,7 @@ class DirectorTest extends SapphireTest
$rootURL = Director::protocolAndHost(); $rootURL = Director::protocolAndHost();
// relative base URLs - you should end them in a / // relative base URLs - you should end them in a /
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/relativebase/'); Director::config()->set('alternate_base_url', '/relativebase/');
$_SERVER['REQUEST_URI'] = "$rootURL/relativebase/sub-page/"; $_SERVER['REQUEST_URI'] = "$rootURL/relativebase/sub-page/";
$this->assertEquals('/relativebase/', Director::baseURL()); $this->assertEquals('/relativebase/', Director::baseURL());
@ -187,7 +192,7 @@ class DirectorTest extends SapphireTest
); );
// absolute base URLs - you should end them in a / // absolute base URLs - you should end them in a /
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', 'http://www.example.org/'); Director::config()->set('alternate_base_url', 'http://www.example.org/');
$_SERVER['REQUEST_URI'] = "http://www.example.org/sub-page/"; $_SERVER['REQUEST_URI'] = "http://www.example.org/sub-page/";
$this->assertEquals('http://www.example.org/', Director::baseURL()); $this->assertEquals('http://www.example.org/', Director::baseURL());
$this->assertEquals('http://www.example.org/', Director::absoluteBaseURL()); $this->assertEquals('http://www.example.org/', Director::absoluteBaseURL());
@ -250,7 +255,6 @@ class DirectorTest extends SapphireTest
public function testIsRelativeUrl() public function testIsRelativeUrl()
{ {
$siteUrl = Director::absoluteBaseURL();
$this->assertFalse(Director::is_relative_url('http://test.com')); $this->assertFalse(Director::is_relative_url('http://test.com'));
$this->assertFalse(Director::is_relative_url('https://test.com')); $this->assertFalse(Director::is_relative_url('https://test.com'));
$this->assertFalse(Director::is_relative_url(' https://test.com/testpage ')); $this->assertFalse(Director::is_relative_url(' https://test.com/testpage '));
@ -341,11 +345,11 @@ class DirectorTest extends SapphireTest
$_COOKIE = array('somekey' => 'cookievalue'); $_COOKIE = array('somekey' => 'cookievalue');
$cookies = Injector::inst()->createWithArgs( $cookies = Injector::inst()->createWithArgs(
'SilverStripe\\Control\\Cookie_Backend', Cookie_Backend::class,
array(array('somekey' => 'sometestcookievalue')) array(array('somekey' => 'sometestcookievalue'))
); );
$getresponse = Director::test( Director::test(
'errorpage?somekey=sometestgetvalue', 'errorpage?somekey=sometestgetvalue',
array('somekey' => 'sometestpostvalue'), array('somekey' => 'sometestpostvalue'),
null, null,
@ -387,10 +391,10 @@ class DirectorTest extends SapphireTest
strtoupper($method), strtoupper($method),
null, null,
null, null,
Injector::inst()->createWithArgs('SilverStripe\\Control\\Cookie_Backend', array($fixture)) Injector::inst()->createWithArgs(Cookie_Backend::class, array($fixture))
); );
$this->assertInstanceOf('SilverStripe\\Control\\HTTPResponse', $getresponse, 'Director::test() returns HTTPResponse'); $this->assertInstanceOf(HTTPResponse::class, $getresponse, 'Director::test() returns HTTPResponse');
$this->assertEquals($fixture['somekey'], $getresponse->getBody(), 'Director::test() ' . $testfunction); $this->assertEquals($fixture['somekey'], $getresponse->getBody(), 'Director::test() ' . $testfunction);
} }
} }
@ -402,6 +406,7 @@ class DirectorTest extends SapphireTest
*/ */
public function testRouteParams() public function testRouteParams()
{ {
/** @var HTTPRequest $request */
Director::test('en-nz/myaction/myid/myotherid', null, null, null, null, null, null, $request); Director::test('en-nz/myaction/myid/myotherid', null, null, null, null, null, null, $request);
$this->assertEquals( $this->assertEquals(
@ -436,7 +441,7 @@ class DirectorTest extends SapphireTest
public function testForceSSLOnSubPagesPattern() public function testForceSSLOnSubPagesPattern()
{ {
$_SERVER['REQUEST_URI'] = Director::baseURL() . Config::inst()->get('SilverStripe\\Security\\Security', 'login_url'); $_SERVER['REQUEST_URI'] = Director::baseURL() . Config::inst()->get(Security::class, 'login_url');
$output = Director::forceSSL(array('/^Security/')); $output = Director::forceSSL(array('/^Security/'));
$this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); $this->assertEquals($output, 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
} }
@ -454,7 +459,7 @@ class DirectorTest extends SapphireTest
public function testForceSSLAlternateDomain() public function testForceSSLAlternateDomain()
{ {
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/'); Director::config()->set('alternate_base_url', '/');
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'admin'; $_SERVER['REQUEST_URI'] = Director::baseURL() . 'admin';
$output = Director::forceSSL(array('/^admin/'), 'secure.mysite.com'); $output = Director::forceSSL(array('/^admin/'), 'secure.mysite.com');
$this->assertEquals($output, 'https://secure.mysite.com/admin'); $this->assertEquals($output, 'https://secure.mysite.com/admin');
@ -565,6 +570,7 @@ class DirectorTest extends SapphireTest
//test that hashes are ignored //test that hashes are ignored
$url = "TestController/returnGetValue?somekey=key"; $url = "TestController/returnGetValue?somekey=key";
$hash = "#test"; $hash = "#test";
/** @var HTTPRequest $request */
$response = Director::test($url . $hash, null, null, null, null, null, null, $request); $response = Director::test($url . $hash, null, null, null, null, null, null, $request);
$this->assertFalse($response->isError()); $this->assertFalse($response->isError());
$this->assertEquals('key', $response->getBody()); $this->assertEquals('key', $response->getBody());
@ -584,25 +590,25 @@ class DirectorTest extends SapphireTest
$processor = new RequestProcessor(array($filter)); $processor = new RequestProcessor(array($filter));
Injector::inst()->registerService($processor, 'SilverStripe\\Control\\RequestProcessor'); Injector::inst()->registerService($processor, RequestProcessor::class);
$response = Director::test('some-dummy-url'); Director::test('some-dummy-url');
$this->assertEquals(1, $filter->preCalls); $this->assertEquals(1, $filter->preCalls);
$this->assertEquals(1, $filter->postCalls); $this->assertEquals(1, $filter->postCalls);
$filter->failPost = true; $filter->failPost = true;
$this->setExpectedException('SilverStripe\\Control\\HTTPResponse_Exception'); $this->setExpectedException(HTTPResponse_Exception::class);
$response = Director::test('some-dummy-url'); Director::test('some-dummy-url');
$this->assertEquals(2, $filter->preCalls); $this->assertEquals(2, $filter->preCalls);
$this->assertEquals(2, $filter->postCalls); $this->assertEquals(2, $filter->postCalls);
$filter->failPre = true; $filter->failPre = true;
$response = Director::test('some-dummy-url'); Director::test('some-dummy-url');
$this->assertEquals(3, $filter->preCalls); $this->assertEquals(3, $filter->preCalls);

View File

@ -7,6 +7,7 @@ use SilverStripe\Dev\TestOnly;
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
private static $url_segment = 'TestController';
private static $allowed_actions = array( private static $allowed_actions = array(
'returnGetValue', 'returnGetValue',

View File

@ -10,6 +10,8 @@ use SilverStripe\Dev\TestOnly;
*/ */
class AllowedController extends Controller implements TestOnly class AllowedController extends Controller implements TestOnly
{ {
private static $url_segment = 'AllowedController';
private static $url_handlers = array( private static $url_handlers = array(
// The double-slash is need here to ensure that // The double-slash is need here to ensure that
'$Action//$ID/$OtherID' => "handleAction", '$Action//$ID/$OtherID' => "handleAction",

View File

@ -9,11 +9,13 @@ use SilverStripe\Forms\FormAction;
class ControllerFormWithAllowedActions extends Controller implements TestOnly class ControllerFormWithAllowedActions extends Controller implements TestOnly
{ {
private static $url_segment = 'ControllerFormWithAllowedActions';
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');
/** /**
* @skipUpgrade * @skipUpgrade
*/ */
public function Form() public function Form()
{ {
return new FormWithAllowedActions( return new FormWithAllowedActions(

View File

@ -14,6 +14,7 @@ use SilverStripe\Forms\FormAction;
*/ */
class FieldController extends Controller implements TestOnly class FieldController extends Controller implements TestOnly
{ {
private static $url_segment = 'FieldController';
private static $allowed_actions = array('TestForm'); private static $allowed_actions = array('TestForm');

View File

@ -14,6 +14,8 @@ class FormActionController extends Controller implements TestOnly
{ {
protected $template = 'BlankPage'; protected $template = 'BlankPage';
private static $url_segment = 'FormActionController';
private static $allowed_actions = array( private static $allowed_actions = array(
'controlleraction', 'controlleraction',
'Form', 'Form',
@ -32,8 +34,8 @@ class FormActionController extends Controller implements TestOnly
} }
/** /**
* @skipUpgrade * @skipUpgrade
*/ */
public function Form() public function Form()
{ {
return new Form( return new Form(

View File

@ -7,17 +7,8 @@ use SilverStripe\Forms\Form;
class FormWithAllowedActions extends Form implements TestOnly class FormWithAllowedActions extends Form implements TestOnly
{ {
private static $allowed_actions = array( protected function buildRequestHandler()
'allowedformaction' => 1,
);
public function allowedformaction()
{ {
return 'allowedformaction'; return FormWithAllowedActionsHandler::create($this);
}
public function disallowedformaction()
{
return 'disallowedformaction';
} }
} }

View File

@ -0,0 +1,27 @@
<?php
namespace SilverStripe\Control\Tests\RequestHandlingTest;
use SilverStripe\Forms\FormRequestHandler;
/**
* Request handler for
* @see FormWithAllowedActions
*/
class FormWithAllowedActionsHandler extends FormRequestHandler
{
private static $allowed_actions = array(
'allowedformaction' => 1,
);
public function allowedformaction()
{
return 'allowedformaction';
}
public function disallowedformaction()
{
return 'disallowedformaction';
}
}

View File

@ -15,6 +15,7 @@ use SilverStripe\View\SSViewer;
*/ */
class TestController extends Controller implements TestOnly class TestController extends Controller implements TestOnly
{ {
private static $url_segment = 'TestController';
private static $allowed_actions = array( private static $allowed_actions = array(
'method', 'method',

View File

@ -10,31 +10,8 @@ use SilverStripe\Forms\Form;
*/ */
class TestForm extends Form implements TestOnly class TestForm extends Form implements TestOnly
{ {
private static $url_handlers = array( protected function buildRequestHandler()
'fields/$FieldName' => 'handleField',
"POST " => "handleSubmission",
"GET " => "handleGet",
);
// These are a different case from those in url_handlers to confirm that it's all case-insensitive
private static $allowed_actions = array(
'handlesubmission',
'handlefield',
'handleget',
);
public function handleField($request)
{ {
return $this->Fields()->dataFieldByName($request->param('FieldName')); return TestFormHandler::create($this);
}
public function handleSubmission($request)
{
return "Form posted";
}
public function handleGet($request)
{
return "Get request on form";
} }
} }

View File

@ -0,0 +1,41 @@
<?php
namespace SilverStripe\Control\Tests\RequestHandlingTest;
use SilverStripe\Forms\FormRequestHandler;
/**
* Handler for
* @see TestForm
*/
class TestFormHandler extends FormRequestHandler
{
private static $url_handlers = array(
'fields/$FieldName' => 'handleField',
"POST " => "handleSubmission",
"GET " => "handleGet",
);
// These are a different case from those in url_handlers to confirm that it's all case-insensitive
private static $allowed_actions = array(
'handlesubmission',
'handlefield',
'handleget',
);
public function handleField($request)
{
return $this->form->Fields()->dataFieldByName($request->param('FieldName'));
}
public function handleSubmission($request)
{
return "Form posted";
}
public function handleGet($request)
{
return "Get request on form";
}
}

View File

@ -463,6 +463,9 @@ class DropdownFieldTest extends SapphireTest
return $foundDisabled; return $foundDisabled;
} }
/**
* @skipUpgrade
*/
public function testValidation() public function testValidation()
{ {
$field = DropdownField::create( $field = DropdownField::create(
@ -475,10 +478,7 @@ class DropdownFieldTest extends SapphireTest
) )
); );
$validator = new RequiredFields(); $validator = new RequiredFields();
/** $form = new Form(null, 'Form', new FieldList($field), new FieldList(), $validator);
* @skipUpgrade
*/
$form = new Form($this, 'Form', new FieldList($field), new FieldList(), $validator);
$field->setValue("One"); $field->setValue("One");
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
$field->setName("TestNew"); //try changing name of field $field->setName("TestNew"); //try changing name of field

View File

@ -29,9 +29,7 @@ class EmbedShortcodeProviderTest extends SapphireTest
public function testYoutube() public function testYoutube()
{ {
/** /** @var Webpage $result */
* @var Webpage $result
*/
$result = Embed::create(self::$test_youtube, array()); $result = Embed::create(self::$test_youtube, array());
self::assertEquals($result->providerName, 'YouTube'); self::assertEquals($result->providerName, 'YouTube');
$embedded = EmbedShortcodeProvider::embedForTemplate($result); $embedded = EmbedShortcodeProvider::embedForTemplate($result);
@ -44,9 +42,7 @@ class EmbedShortcodeProviderTest extends SapphireTest
public function testSoundcloud() public function testSoundcloud()
{ {
/** /** @var Webpage $result */
* @var Webpage $result
*/
$result = Embed::create(self::$test_soundcloud, array()); $result = Embed::create(self::$test_soundcloud, array());
self::assertEquals($result->providerName, 'SoundCloud'); self::assertEquals($result->providerName, 'SoundCloud');
$embedded = EmbedShortcodeProvider::embedForTemplate($result); $embedded = EmbedShortcodeProvider::embedForTemplate($result);

View File

@ -14,12 +14,11 @@ class FileFieldTest extends FunctionalTest
/** /**
* Test a valid upload of a required file in a form. Error is set to 0, as the upload went well * Test a valid upload of a required file in a form. Error is set to 0, as the upload went well
*
* @skipUpgrade
*/ */
public function testUploadRequiredFile() public function testUploadRequiredFile()
{ {
/**
* @skipUpgrade
*/
$form = new Form( $form = new Form(
new Controller(), new Controller(),
'Form', 'Form',
@ -42,12 +41,10 @@ class FileFieldTest extends FunctionalTest
/** /**
* Test different scenarii for a failed upload : an error occured, no files where provided * Test different scenarii for a failed upload : an error occured, no files where provided
* @skipUpgrade
*/ */
public function testUploadMissingRequiredFile() public function testUploadMissingRequiredFile()
{ {
/**
* @skipUpgrade
*/
$form = new Form( $form = new Form(
new Controller(), new Controller(),
'Form', 'Form',

View File

@ -362,7 +362,7 @@ class FormFieldTest extends SapphireTest
{ {
$field = new FormField('MyField', 'My Field'); $field = new FormField('MyField', 'My Field');
$validator = new RequiredFields('MyField'); $validator = new RequiredFields('MyField');
$form = new Form(new Controller(), 'TestForm', new FieldList($field), new FieldList(), $validator); $form = new Form(null, 'TestForm', new FieldList($field), new FieldList(), $validator);
$form->validationResult(); $form->validationResult();
$schema = $field->getSchemaState(); $schema = $field->getSchemaState();
$this->assertEquals( $this->assertEquals(

View File

@ -37,7 +37,7 @@ class FormScaffolderTest extends SapphireTest
{ {
$article = new Article; $article = new Article;
$fields = $article->getCMSFields(); $fields = $article->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article); $form->loadDataFrom($article);
$this->assertTrue( $this->assertTrue(
@ -67,7 +67,7 @@ class FormScaffolderTest extends SapphireTest
$article1 = $this->objFromFixture(Article::class, 'article1'); $article1 = $this->objFromFixture(Article::class, 'article1');
$fields = $article1->getCMSFields(); $fields = $article1->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1); $form->loadDataFrom($article1);
$this->assertNotNull( $this->assertNotNull(
@ -101,7 +101,7 @@ class FormScaffolderTest extends SapphireTest
$article1 = $this->objFromFixture(Article::class, 'article1'); $article1 = $this->objFromFixture(Article::class, 'article1');
$fields = $article1->getCMSFields(); $fields = $article1->getCMSFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1); $form->loadDataFrom($article1);
$this->assertNotNull( $this->assertNotNull(
@ -119,7 +119,7 @@ class FormScaffolderTest extends SapphireTest
'restrictFields' => array('Title') 'restrictFields' => array('Title')
) )
); );
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1); $form->loadDataFrom($article1);
$this->assertNotNull( $this->assertNotNull(
@ -141,7 +141,7 @@ class FormScaffolderTest extends SapphireTest
'fieldClasses' => array('Title' => 'SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField') 'fieldClasses' => array('Title' => 'SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField')
) )
); );
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom($article1); $form->loadDataFrom($article1);
$this->assertNotNull( $this->assertNotNull(
@ -157,7 +157,7 @@ class FormScaffolderTest extends SapphireTest
public function testGetFormFields() public function testGetFormFields()
{ {
$fields = Article::singleton()->getFrontEndFields(); $fields = Article::singleton()->getFrontEndFields();
$form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form = new Form(null, 'TestForm', $fields, new FieldList());
$form->loadDataFrom(singleton(Article::class)); $form->loadDataFrom(singleton(Article::class));
$this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default'); $this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default');

View File

@ -20,16 +20,16 @@ class FormSchemaTest extends SapphireTest
public function testGetSchema() public function testGetSchema()
{ {
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList()); $form = new Form(null, 'TestForm', new FieldList(), new FieldList());
$formSchema = new FormSchema(); $formSchema = new FormSchema();
$expected = [ $expected = [
'name' => 'TestForm', 'name' => 'TestForm',
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'attributes' => [ 'attributes' => [
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'enctype' => 'application/x-www-form-urlencoded', 'enctype' => 'application/x-www-form-urlencoded',
'target' => null, 'target' => null,
@ -67,7 +67,7 @@ class FormSchemaTest extends SapphireTest
public function testGetState() public function testGetState()
{ {
$form = new Form(new Controller(), 'TestForm', new FieldList(), new FieldList()); $form = new Form(null, 'TestForm', new FieldList(), new FieldList());
$formSchema = new FormSchema(); $formSchema = new FormSchema();
$expected = [ $expected = [
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
@ -92,7 +92,7 @@ class FormSchemaTest extends SapphireTest
{ {
$fields = new FieldList(); $fields = new FieldList();
$actions = new FieldList(); $actions = new FieldList();
$form = new Form(new Controller(), 'TestForm', $fields, $actions); $form = new Form(null, 'TestForm', $fields, $actions);
$form->sessionMessage('All saved', 'good'); $form->sessionMessage('All saved', 'good');
$formSchema = new FormSchema(); $formSchema = new FormSchema();
$expected = [ $expected = [
@ -122,7 +122,7 @@ class FormSchemaTest extends SapphireTest
$fields = new FieldList(new TextField('Title')); $fields = new FieldList(new TextField('Title'));
$actions = new FieldList(); $actions = new FieldList();
$validator = new RequiredFields('Title'); $validator = new RequiredFields('Title');
$form = new Form(new Controller(), 'TestForm', $fields, $actions, $validator); $form = new Form(null, 'TestForm', $fields, $actions, $validator);
$form->loadDataFrom( $form->loadDataFrom(
[ [
'Title' => null, 'Title' => null,
@ -159,10 +159,13 @@ class FormSchemaTest extends SapphireTest
$this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($state)); $this->assertJsonStringEqualsJsonString(json_encode($expected), json_encode($state));
} }
/**
* @skipUpgrade
*/
public function testGetNestedSchema() public function testGetNestedSchema()
{ {
$form = new Form( $form = new Form(
new Controller(), null,
'TestForm', 'TestForm',
new FieldList(new TextField("Name")), new FieldList(new TextField("Name")),
new FieldList( new FieldList(
@ -180,17 +183,14 @@ class FormSchemaTest extends SapphireTest
) )
); );
$formSchema = new FormSchema(); $formSchema = new FormSchema();
/**
* @skipUpgrade
*/
$expected = [ $expected = [
'name' => 'TestForm', 'name' => 'TestForm',
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'attributes' => [ 'attributes' => [
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'enctype' => 'application/x-www-form-urlencoded', 'enctype' => 'application/x-www-form-urlencoded',
'target' => null, 'target' => null,
@ -392,7 +392,7 @@ class FormSchemaTest extends SapphireTest
public function testSchemaValidation() public function testSchemaValidation()
{ {
$form = new Form( $form = new Form(
new Controller(), null,
'TestForm', 'TestForm',
new FieldList( new FieldList(
TextField::create("Name") TextField::create("Name")
@ -409,12 +409,12 @@ class FormSchemaTest extends SapphireTest
$expected = [ $expected = [
'name' => 'TestForm', 'name' => 'TestForm',
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'attributes' => 'attributes' =>
[ [
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
'action' => 'Controller/TestForm', 'action' => null,
'method' => 'POST', 'method' => 'POST',
'enctype' => 'application/x-www-form-urlencoded', 'enctype' => 'application/x-www-form-urlencoded',
'target' => null, 'target' => null,

View File

@ -757,10 +757,13 @@ class FormTest extends FunctionalTest
$this->assertEquals('bar', $attrs['foo']); $this->assertEquals('bar', $attrs['foo']);
} }
/**
* @skipUpgrade
*/
public function testButtonClicked() public function testButtonClicked()
{ {
$form = $this->getStubForm(); $form = $this->getStubForm();
$action = $form->buttonClicked(); $action = $form->getRequestHandler()->buttonClicked();
$this->assertNull($action); $this->assertNull($action);
$controller = new FormTest\TestController(); $controller = new FormTest\TestController();
@ -776,13 +779,10 @@ class FormTest extends FunctionalTest
) )
); );
$form->httpSubmission($request); $form->getRequestHandler()->httpSubmission($request);
$button = $form->buttonClicked(); $button = $form->getRequestHandler()->buttonClicked();
$this->assertInstanceOf('SilverStripe\\Forms\\FormAction', $button); $this->assertInstanceOf(FormAction::class, $button);
$this->assertEquals('doSubmit', $button->actionName()); $this->assertEquals('doSubmit', $button->actionName());
/**
* @skipUpgrade
*/
$form = new Form( $form = new Form(
$controller, $controller,
'Form', 'Form',
@ -799,9 +799,9 @@ class FormTest extends FunctionalTest
) )
); );
$form->httpSubmission($request); $form->getRequestHandler()->httpSubmission($request);
$button = $form->buttonClicked(); $button = $form->getRequestHandler()->buttonClicked();
$this->assertInstanceOf('SilverStripe\\Forms\\FormAction', $button); $this->assertInstanceOf(FormAction::class, $button);
$this->assertEquals('doSubmit', $button->actionName()); $this->assertEquals('doSubmit', $button->actionName());
} }
@ -814,7 +814,7 @@ class FormTest extends FunctionalTest
new FieldList(), new FieldList(),
new FieldList(new FormAction('actionName', 'Action')) new FieldList(new FormAction('actionName', 'Action'))
); );
$this->assertTrue($form->checkAccessAction('actionName')); $this->assertTrue($form->getRequestHandler()->checkAccessAction('actionName'));
$form = new Form( $form = new Form(
$controller, $controller,
@ -822,7 +822,7 @@ class FormTest extends FunctionalTest
new FieldList(new FormAction('inlineAction', 'Inline action')), new FieldList(new FormAction('inlineAction', 'Inline action')),
new FieldList() new FieldList()
); );
$this->assertTrue($form->checkAccessAction('inlineAction')); $this->assertTrue($form->getRequestHandler()->checkAccessAction('inlineAction'));
} }
public function testAttributesHTML() public function testAttributesHTML()

View File

@ -62,7 +62,7 @@ class GridFieldDeleteActionTest extends SapphireTest
$this->list = new DataList(Team::class); $this->list = new DataList(Team::class);
$config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction()); $config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction());
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config); $this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
$this->form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList()); $this->form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList());
} }
public function testDontShowDeleteButtons() public function testDontShowDeleteButtons()
@ -185,7 +185,7 @@ class GridFieldDeleteActionTest extends SapphireTest
$config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction(true)); $config = GridFieldConfig::create()->addComponent(new GridFieldDeleteAction(true));
$gridField = new GridField('testfield', 'testfield', $this->list, $config); $gridField = new GridField('testfield', 'testfield', $this->list, $config);
$form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList()); $form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList());
$stateID = 'testGridStateActionField'; $stateID = 'testGridStateActionField';
Session::set( Session::set(

View File

@ -57,7 +57,7 @@ class GridFieldEditButtonTest extends SapphireTest
$this->list = new DataList(Team::class); $this->list = new DataList(Team::class);
$config = GridFieldConfig::create()->addComponent(new GridFieldEditButton()); $config = GridFieldConfig::create()->addComponent(new GridFieldEditButton());
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config); $this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
$this->form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList()); $this->form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList());
} }
public function testShowEditLinks() public function testShowEditLinks()

View File

@ -59,7 +59,7 @@ class GridFieldPaginatorTest extends FunctionalTest
new GridFieldPageCount('toolbar-header-right') new GridFieldPageCount('toolbar-header-right')
); );
$this->gridField = new GridField('testfield', 'testfield', $this->list, $config); $this->gridField = new GridField('testfield', 'testfield', $this->list, $config);
$this->form = new Form(new Controller(), 'mockform', new FieldList(array($this->gridField)), new FieldList()); $this->form = new Form(null, 'mockform', new FieldList(array($this->gridField)), new FieldList());
} }
public function testThereIsPaginatorWhenMoreThanOnePage() public function testThereIsPaginatorWhenMoreThanOnePage()

View File

@ -11,7 +11,6 @@ use SilverStripe\Forms\Tests\GridField\GridFieldSortableHeaderTest\Mom;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
@ -32,6 +31,8 @@ class GridFieldSortableHeaderTest extends SapphireTest
/** /**
* Tests that the appropriate sortable headers are generated * Tests that the appropriate sortable headers are generated
*
* @skipUpgrade
*/ */
public function testRenderHeaders() public function testRenderHeaders()
{ {
@ -39,10 +40,7 @@ class GridFieldSortableHeaderTest extends SapphireTest
// Generate sortable header and extract HTML // Generate sortable header and extract HTML
$list = new DataList(Team::class); $list = new DataList(Team::class);
$config = new GridFieldConfig_RecordEditor(); $config = new GridFieldConfig_RecordEditor();
/** $form = new Form(null, 'Form', new FieldList(), new FieldList());
* @skipUpgrade
*/
$form = new Form(Controller::curr(), 'Form', new FieldList(), new FieldList());
$gridField = new GridField('testfield', 'testfield', $list, $config); $gridField = new GridField('testfield', 'testfield', $list, $config);
$gridField->setForm($form); $gridField->setForm($form);
$compontent = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class); $compontent = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Forms\Tests\GridField; namespace SilverStripe\Forms\Tests\GridField;
use SilverStripe\Control\Controller;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
@ -415,7 +414,7 @@ class GridFieldTest extends SapphireTest
// $obj = new GridField('testfield', 'testfield', ArrayList::create(), $config); // $obj = new GridField('testfield', 'testfield', ArrayList::create(), $config);
// $id = 'testGridStateActionField'; // $id = 'testGridStateActionField';
// Session::set($id, array('grid'=>'', 'actionName'=>'jump')); // Session::set($id, array('grid'=>'', 'actionName'=>'jump'));
// $form = new Form(new Controller(), 'mockform', new FieldList(array($obj)), new FieldList()); // $form = new Form(null, 'mockform', new FieldList(array($obj)), new FieldList());
// $request = new HTTPRequest('POST', 'url'); // $request = new HTTPRequest('POST', 'url');
// $obj->gridFieldAlterAction(array('StateID'=>$id), $form, $request); // $obj->gridFieldAlterAction(array('StateID'=>$id), $form, $request);
} }
@ -460,7 +459,7 @@ class GridFieldTest extends SapphireTest
) )
); );
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config); $field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList()); $form = new Form(null, 'testform', new FieldList(array($field)), new FieldList());
$this->assertContains( $this->assertContains(
"<div class=\"right\">rightone\nrighttwo</div><div class=\"left\">left</div>", "<div class=\"right\">rightone\nrighttwo</div><div class=\"left\">left</div>",
@ -496,7 +495,7 @@ class GridFieldTest extends SapphireTest
) )
); );
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config); $field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList()); $form = new Form(null, 'testform', new FieldList(array($field)), new FieldList());
$this->assertContains( $this->assertContains(
"<div>first\n<strong>second</strong></div>", "<div>first\n<strong>second</strong></div>",
@ -532,7 +531,7 @@ class GridFieldTest extends SapphireTest
) )
); );
$field = new GridField('testfield', 'testfield', ArrayList::create(), $config); $field = new GridField('testfield', 'testfield', ArrayList::create(), $config);
$form = new Form(new Controller(), 'testform', new FieldList(array($field)), new FieldList()); $form = new Form(null, 'testform', new FieldList(array($field)), new FieldList());
$this->setExpectedException('LogicException'); $this->setExpectedException('LogicException');
$field->FieldHolder(); $field->FieldHolder();
@ -573,7 +572,7 @@ class GridFieldTest extends SapphireTest
$config = new GridFieldConfig(); $config = new GridFieldConfig();
$config->addComponent(new GridFieldDataColumns()); $config->addComponent(new GridFieldDataColumns());
$obj = new GridField('testfield', 'testfield', $list, $config); $obj = new GridField('testfield', 'testfield', $list, $config);
$form = new Form(new Controller(), 'mockform', new FieldList(array($obj)), new FieldList()); $form = new Form(null, 'mockform', new FieldList(array($obj)), new FieldList());
$content = new CSSContentParser($obj->FieldHolder()); $content = new CSSContentParser($obj->FieldHolder());
$members = $content->getBySelector('.ss-gridfield-item tr'); $members = $content->getBySelector('.ss-gridfield-item tr');

View File

@ -7,6 +7,7 @@ use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridField_URLHandler; use SilverStripe\Forms\GridField\GridField_URLHandler;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -19,13 +20,16 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
private static $allowed_actions = array('Form', 'showform', 'testpage', 'handleItem'); private static $allowed_actions = array('Form', 'showform', 'testpage', 'handleItem');
/**
* @var GridField
*/
protected $gridField; protected $gridField;
/**
* @skipUpgrade
*/
public function getURLHandlers($gridField) public function getURLHandlers($gridField)
{ {
/**
* @skipUpgrade
*/
return array( return array(
'showform' => 'showform', 'showform' => 'showform',
'testpage' => 'testpage', 'testpage' => 'testpage',
@ -44,7 +48,7 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
); );
} }
public function Link() public function Link($action = null)
{ {
return $this->gridField->Link(); return $this->gridField->Link();
} }
@ -54,12 +58,12 @@ class TestComponent extends RequestHandler implements GridField_URLHandler
return "<head>" . SSViewer::get_base_tag("") . "</head>" . $this->Form($gridField, $request)->forTemplate(); return "<head>" . SSViewer::get_base_tag("") . "</head>" . $this->Form($gridField, $request)->forTemplate();
} }
/**
* @skipUpgrade
*/
public function Form($gridField, $request) public function Form($gridField, $request)
{ {
$this->gridField = $gridField; $this->gridField = $gridField;
/**
* @skipUpgrade
*/
return new Form( return new Form(
$this, $this,
'Form', 'Form',

View File

@ -28,7 +28,7 @@ class TestComponent_ItemRequest extends RequestHandler
parent::__construct(); parent::__construct();
} }
public function Link() public function Link($action = null)
{ {
return $this->link; return $this->link;
} }
@ -40,12 +40,10 @@ class TestComponent_ItemRequest extends RequestHandler
public function Form() public function Form()
{ {
/** /** @skipUpgrade */
* @skipUpgrade
*/
return new Form( return new Form(
$this, $this,
'Form', Form::DEFAULT_NAME,
new FieldList( new FieldList(
new TextField("Test") new TextField("Test")
), ),

View File

@ -22,6 +22,10 @@ class TestController extends Controller implements TestOnly
private static $allowed_actions = array('Form'); private static $allowed_actions = array('Form');
/**
* @skipUpgrade
* @return Form
*/
public function Form() public function Form()
{ {
$gridConfig = GridFieldConfig::create(); $gridConfig = GridFieldConfig::create();
@ -30,9 +34,6 @@ class TestController extends Controller implements TestOnly
$gridData = new ArrayList(); $gridData = new ArrayList();
$gridField = new GridField('Grid', 'My grid', $gridData, $gridConfig); $gridField = new GridField('Grid', 'My grid', $gridData, $gridConfig);
/**
* @skipUpgrade
*/
return new Form( return new Form(
$this, $this,
'Form', 'Form',

View File

@ -34,6 +34,9 @@ class OptionsetFieldTest extends SapphireTest
); );
} }
/**
* @skipUpgrade
*/
public function testValidation() public function testValidation()
{ {
$field = OptionsetField::create( $field = OptionsetField::create(
@ -46,10 +49,7 @@ class OptionsetFieldTest extends SapphireTest
) )
); );
$validator = new RequiredFields('Test'); $validator = new RequiredFields('Test');
/** $form = new Form(null, 'Form', new FieldList($field), new FieldList(), $validator);
* @skipUpgrade
*/
$form = new Form($this, 'Form', new FieldList($field), new FieldList(), $validator);
$field->setValue("One"); $field->setValue("One");
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));

View File

@ -389,8 +389,8 @@ class SecurityTest extends FunctionalTest
$expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword'); $expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword');
$this->assertEquals(302, $expiredResponse->getStatusCode()); $this->assertEquals(302, $expiredResponse->getStatusCode());
$this->assertEquals( $this->assertEquals(
'/Security/changepassword', Director::absoluteURL('Security/changepassword').'?BackURL=test%2Flink',
$expiredResponse->getHeader('Location') Director::absoluteURL($expiredResponse->getHeader('Location'))
); );
$this->assertEquals( $this->assertEquals(
$this->idFromFixture(Member::class, 'expiredpassword'), $this->idFromFixture(Member::class, 'expiredpassword'),
@ -456,7 +456,10 @@ class SecurityTest extends FunctionalTest
// Check. // Check.
$response = $this->get('Security/changepassword/?m='.$admin->ID.'&t=' . $token); $response = $this->get('Security/changepassword/?m='.$admin->ID.'&t=' . $token);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
$this->assertEquals(Director::baseUrl() . 'Security/changepassword', $response->getHeader('Location')); $this->assertEquals(
Director::absoluteURL('Security/changepassword'),
Director::absoluteURL($response->getHeader('Location'))
);
// Follow redirection to form without hash in GET parameter // Follow redirection to form without hash in GET parameter
$response = $this->get('Security/changepassword'); $response = $this->get('Security/changepassword');
@ -756,9 +759,6 @@ class SecurityTest extends FunctionalTest
{ {
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result'); $result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result');
if ($result) { if ($result) {
/**
* @var ValidationResult $resultObj
*/
return unserialize($result); return unserialize($result);
} }
return null; return null;