mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch 'origin/3.1'
Conflicts: templates/forms/CheckboxSetField.ss templates/forms/FormField_holder.ss templates/forms/OptionsetField.ss
This commit is contained in:
commit
0a79ac3592
@ -449,6 +449,9 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
$title = $this->Title();
|
||||
if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
|
||||
if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
|
||||
|
||||
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
|
||||
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
<div class="breadcrumbs-wrapper" data-pjax-fragment="Breadcrumbs">
|
||||
<h2 id="page-title-heading">
|
||||
<% if ToplevelController %>
|
||||
<% if $ToplevelController %>
|
||||
<span class="section-icon icon icon-16 icon-{$ToplevelController.MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% else_if Controller %>
|
||||
<% else_if $Controller %>
|
||||
<span class="section-icon icon icon-16 icon-{$Controller.MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% else %>
|
||||
<span class="section-icon icon icon-16 icon-{$MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% end_if %>
|
||||
|
||||
<% loop Breadcrumbs %>
|
||||
<% if Last %>
|
||||
<% loop $Breadcrumbs %>
|
||||
<% if $Last %>
|
||||
<span class="cms-panel-link crumb last">$Title.XML</span>
|
||||
<% else %>
|
||||
<a class="cms-panel-link crumb" href="$Link">$Title.XML</a>
|
||||
|
@ -1,18 +1,18 @@
|
||||
<div id="settings-controller-cms-content" class="cms-content center cms-tabset $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content CurrentForm">
|
||||
|
||||
<div class="cms-content-header north">
|
||||
<% with EditForm %>
|
||||
<% with $EditForm %>
|
||||
<div class="cms-content-header-info">
|
||||
<% with Controller %>
|
||||
<% with $Controller %>
|
||||
<% include CMSBreadcrumbs %>
|
||||
<% end_with %>
|
||||
</div>
|
||||
<% if Fields.hasTabset %>
|
||||
<% with Fields.fieldByName('Root') %>
|
||||
<% if $Fields.hasTabset %>
|
||||
<% with $Fields.fieldByName('Root') %>
|
||||
<div class="cms-content-header-tabs">
|
||||
<ul class="cms-tabset-nav-primary">
|
||||
<% loop Tabs %>
|
||||
<li<% if extraClass %> class="$extraClass"<% end_if %>><a href="#$id">$Title</a></li>
|
||||
<% loop $Tabs %>
|
||||
<li<% if $extraClass %> class="$extraClass"<% end_if %>><a href="#$id">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -5,12 +5,12 @@
|
||||
<%-- e.g. through LeftAndMain_EditForm.ss. --%>
|
||||
|
||||
<div $AttributesHTML>
|
||||
<% loop Tabs %>
|
||||
<% if Tabs %>
|
||||
<% loop $Tabs %>
|
||||
<% if $Tabs %>
|
||||
$FieldHolder
|
||||
<% else %>
|
||||
<div $AttributesHTML>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<% if Backlink %>
|
||||
<% if $Backlink %>
|
||||
<div class="cms_backlink">
|
||||
<a class="backlink ss-ui-button cms-panel-link" data-icon="back" href="$Backlink">
|
||||
<% _t('BackLink_Button.ss.Back', 'Back') %>
|
||||
</a>
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<% if ToplevelController %>
|
||||
<% if $ToplevelController %>
|
||||
<span class="section-icon icon icon-16 icon-{$ToplevelController.MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% else_if Controller %>
|
||||
<% else_if $Controller %>
|
||||
<span class="section-icon icon icon-16 icon-{$Controller.MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% else %>
|
||||
<span class="section-icon icon icon-16 icon-{$MenuCurrentItem.Code.LowerCase}"></span>
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<% with EditorToolbar %>
|
||||
<% with $EditorToolbar %>
|
||||
<div class="mceToolbarExternal" id="mce_editor_toolbar">
|
||||
<table width="100%" border="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
<% loop Buttons %>
|
||||
<% if Type = button %>
|
||||
<% loop $Buttons %>
|
||||
<% if $Type = button %>
|
||||
<a href="#$Command">
|
||||
<img width="20" height="20" class="mceButtonNormal" title="$Title" alt="$Title" src="$Icon" id="mce_editor_$IDSegment" />
|
||||
</a>
|
||||
<% else_if Type = dropdown %>
|
||||
<% else_if $Type = dropdown %>
|
||||
<select name="$Command" class="mceSelectList" id="mce_editor_$IDSegment">$Options</select>
|
||||
<% else_if Type = separator %>
|
||||
<% else_if $Type = separator %>
|
||||
<img width="1" height="15" class="mceSeparatorLine" src="{$MceRoot}themes/advanced/images/separator.gif" alt="|" />
|
||||
<% else_if Type = break %>
|
||||
<% else_if $Type = break %>
|
||||
<br />
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
|
@ -1,19 +1,19 @@
|
||||
<% if IncludeFormTag %>
|
||||
<% if $IncludeFormTag %>
|
||||
<form $FormAttributes data-layout-type="border">
|
||||
<% end_if %>
|
||||
<div class="cms-content-header north">
|
||||
<div class="cms-content-header-info">
|
||||
<% include BackLink_Button %>
|
||||
<% with Controller %>
|
||||
<% with $Controller %>
|
||||
<% include CMSBreadcrumbs %>
|
||||
<% end_with %>
|
||||
</div>
|
||||
<% if Fields.hasTabset %>
|
||||
<% with Fields.fieldByName('Root') %>
|
||||
<% if $Fields.hasTabset %>
|
||||
<% with $Fields.fieldByName('Root') %>
|
||||
<div class="cms-content-header-tabs cms-tabset-nav-primary">
|
||||
<ul>
|
||||
<% loop Tabs %>
|
||||
<li<% if extraClass %> class="$extraClass"<% end_if %>><a href="#$id">$Title</a></li>
|
||||
<% loop $Tabs %>
|
||||
<li<% if $extraClass %> class="$extraClass"<% end_if %>><a href="#$id">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
</div>
|
||||
@ -23,20 +23,20 @@
|
||||
<!-- <div class="cms-content-search">...</div> -->
|
||||
</div>
|
||||
|
||||
<% with Controller %>
|
||||
<% with $Controller %>
|
||||
$EditFormTools
|
||||
<% end_with %>
|
||||
|
||||
<div class="cms-content-fields center <% if not $Fields.hasTabset %>cms-panel-padded<% end_if %>">
|
||||
<% if Message %>
|
||||
<% if $Message %>
|
||||
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
|
||||
<% else %>
|
||||
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
|
||||
<% end_if %>
|
||||
|
||||
<fieldset>
|
||||
<% if Legend %><legend>$Legend</legend><% end_if %>
|
||||
<% loop Fields %>
|
||||
<% if $Legend %><legend>$Legend</legend><% end_if %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
<div class="clear"><!-- --></div>
|
||||
@ -44,12 +44,12 @@
|
||||
</div>
|
||||
|
||||
<div class="cms-content-actions cms-content-controls south">
|
||||
<% if Actions %>
|
||||
<% if $Actions %>
|
||||
<div class="Actions">
|
||||
<% loop Actions %>
|
||||
<% loop $Actions %>
|
||||
$Field
|
||||
<% end_loop %>
|
||||
<% if Controller.LinkPreview %>
|
||||
<% if $Controller.LinkPreview %>
|
||||
<a href="$Controller.LinkPreview" class="cms-preview-toggle-link ss-ui-button" data-icon="preview">
|
||||
<% _t('LeftAndMain.PreviewButton', 'Preview') %> »
|
||||
</a>
|
||||
@ -57,6 +57,6 @@
|
||||
</div>
|
||||
<% end_if %>
|
||||
</div>
|
||||
<% if IncludeFormTag %>
|
||||
<% if $IncludeFormTag %>
|
||||
</form>
|
||||
<% end_if %>
|
||||
|
@ -2,18 +2,18 @@
|
||||
<div class="cms-logo-header north">
|
||||
<div class="cms-logo">
|
||||
<a href="$ApplicationLink" target="_blank" title="$ApplicationName (Version - $CMSVersion)">
|
||||
$ApplicationName <% if CMSVersion %><abbr class="version">$CMSVersion</abbr><% end_if %>
|
||||
$ApplicationName <% if $CMSVersion %><abbr class="version">$CMSVersion</abbr><% end_if %>
|
||||
</a>
|
||||
<span><% if SiteConfig %>$SiteConfig.Title<% else %>$ApplicationName<% end_if %></span>
|
||||
<span><% if $SiteConfig %>$SiteConfig.Title<% else %>$ApplicationName<% end_if %></span>
|
||||
</div>
|
||||
|
||||
<div class="cms-login-status">
|
||||
<a href="Security/logout" class="logout-link" title="<% _t('LeftAndMain_Menu.ss.LOGOUT','Log out') %>"><% _t('LeftAndMain_Menu.ss.LOGOUT','Log out') %></a>
|
||||
<% with CurrentMember %>
|
||||
<% with $CurrentMember %>
|
||||
<span>
|
||||
<% _t('LeftAndMain_Menu.ss.Hello','Hi') %>
|
||||
<a href="{$AbsoluteBaseURL}admin/myprofile" class="profile-link">
|
||||
<% if FirstName && Surname %>$FirstName $Surname<% else_if FirstName %>$FirstName<% else %>$Email<% end_if %>
|
||||
<% if $FirstName && $Surname %>$FirstName $Surname<% else_if $FirstName %>$FirstName<% else %>$Email<% end_if %>
|
||||
</a>
|
||||
</span>
|
||||
<% end_with %>
|
||||
@ -22,9 +22,9 @@
|
||||
|
||||
<div class="cms-panel-content center">
|
||||
<ul class="cms-menu-list">
|
||||
<% loop MainMenu %>
|
||||
<li class="$LinkingMode $FirstLast <% if LinkingMode == 'link' %><% else %>opened<% end_if %>" id="Menu-$Code" title="$Title.ATT">
|
||||
<a href="$Link" <% if Code == 'Help' %>target="_blank"<% end_if%>>
|
||||
<% loop $MainMenu %>
|
||||
<li class="$LinkingMode $FirstLast <% if $LinkingMode == 'link' %><% else %>opened<% end_if %>" id="Menu-$Code" title="$Title.ATT">
|
||||
<a href="$Link" <% if $Code == 'Help' %>target="_blank"<% end_if%>>
|
||||
<span class="icon icon-16 icon-{$Code.LowerCase}"> </span>
|
||||
<span class="text">$Title</span>
|
||||
</a>
|
||||
|
@ -19,13 +19,13 @@
|
||||
</select>
|
||||
</span>
|
||||
|
||||
<% if Items %>
|
||||
<% if Items.Count < 5 %>
|
||||
<% if $Items %>
|
||||
<% if $Items.Count < 5 %>
|
||||
<fieldset id="preview-states" class="cms-preview-states switch-states size_{$Items.Count}">
|
||||
<div class="switch">
|
||||
<% loop Items %>
|
||||
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if isActive %>checked<% end_if %>>
|
||||
<label for="$Title"<% if isActive %> class="active"<% end_if %>><span>$Title</span></label>
|
||||
<% loop $Items %>
|
||||
<input id="$Title" data-name="$Name" class="state-name $FirstLast" data-link="$Link" name="view" type="radio" <% if $isActive %>checked<% end_if %>>
|
||||
<label for="$Title"<% if $isActive %> class="active"<% end_if %>><span>$Title</span></label>
|
||||
<% end_loop %>
|
||||
<span class="slide-button"></span>
|
||||
</div>
|
||||
@ -33,8 +33,8 @@
|
||||
<% else %>
|
||||
<span id="preview-state-dropdown" class="cms-preview-states field dropdown">
|
||||
<select title="<% _t('SilverStripeNavigator.PreviewState', 'Preview State') %>" id="preview-states" class="preview-state dropdown nolabel" autocomplete="off" name="preview-state">
|
||||
<% loop Items %>
|
||||
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" <% if isActive %>selected<% end_if %>>
|
||||
<% loop $Items %>
|
||||
<option name="$Name" data-name="$Name" data-link="$Link" class="state-name $FirstLast" value="$Link" <% if $isActive %>selected<% end_if %>>
|
||||
$Title
|
||||
</option>
|
||||
<% end_loop %>
|
||||
@ -42,4 +42,4 @@
|
||||
</span>
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="cms-content-header-info">
|
||||
<h2>
|
||||
<% include CMSSectionIcon %>
|
||||
<% if SectionTitle %>
|
||||
<% if $SectionTitle %>
|
||||
$SectionTitle
|
||||
<% else %>
|
||||
<% _t('ModelAdmin.Title', 'Data Models') %>
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
<div class="cms-content-header-tabs cms-tabset-nav-primary ss-ui-tabs-nav">
|
||||
<ul>
|
||||
<% loop ManagedModelTabs %>
|
||||
<% loop $ManagedModelTabs %>
|
||||
<li class="tab-$ClassName $LinkOrCurrent<% if $LinkOrCurrent == 'current' %> ui-tabs-active<% end_if %>">
|
||||
<a href="$Link" class="cms-panel-link" title="Form_EditForm">$Title</a>
|
||||
</li>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="details" id="SpecDetailsFor{$ModelName}">
|
||||
<h4><% sprintf(_t('ModelAdmin_ImportSpec.ss.IMPORTSPECTITLE', 'Specification for %s'),$ModelName) %></h4>
|
||||
<h5><% _t('ModelAdmin_ImportSpec.ss.IMPORTSPECFIELDS', 'Database columns') %></h5>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<dl>
|
||||
<dt><em>$Name</em></dt>
|
||||
<dd>$Description</dd>
|
||||
@ -11,7 +11,7 @@
|
||||
<% end_loop %>
|
||||
|
||||
<h5><% _t('ModelAdmin_ImportSpec.ss.IMPORTSPECRELATIONS', 'Relations') %></h5>
|
||||
<% loop Relations %>
|
||||
<% loop $Relations %>
|
||||
<dl>
|
||||
<dt><em>$Name</em></dt>
|
||||
<dd>$Description</dd>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h3 class="cms-panel-header"><% _t('ModelAdmin_Tools.ss.FILTER', 'Filter') %></h3>
|
||||
$SearchForm
|
||||
|
||||
<% if ImportForm %>
|
||||
<% if $ImportForm %>
|
||||
<h3 class="cms-panel-header"><% _t('ModelAdmin_Tools.ss.IMPORT', 'Import') %></h3>
|
||||
$ImportForm
|
||||
<% end_if %>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<h3><% _t('ModelSidebar.ss.SEARCHLISTINGS','Search') %></h3>
|
||||
$SearchForm
|
||||
|
||||
<% if ImportForm %>
|
||||
<% if $ImportForm %>
|
||||
<h3><% _t('ModelSidebar.ss.IMPORT_TAB_HEADER', 'Import') %></h3>
|
||||
$ImportForm
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
|
@ -352,7 +352,7 @@ class RestfulService extends ViewableData {
|
||||
$match[1] = preg_replace_callback(
|
||||
'/(?<=^|[\x09\x20\x2D])./',
|
||||
create_function('$matches', 'return strtoupper($matches[0]);'),
|
||||
strtolower(trim($match[1]))
|
||||
trim($match[1])
|
||||
);
|
||||
if( isset($headers[$match[1]]) ) {
|
||||
if (!is_array($headers[$match[1]])) {
|
||||
|
121
core/startup/ErrorControlChain.php
Normal file
121
core/startup/ErrorControlChain.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ErrorControlChain
|
||||
*
|
||||
* Runs a set of steps, optionally suppressing (but recording) any errors (even fatal ones) that occur in each step.
|
||||
* If an error does occur, subsequent steps are normally skipped, but can optionally be run anyway
|
||||
*
|
||||
* Normal errors are suppressed even past the end of the chain. Fatal errors are only suppressed until the end
|
||||
* of the chain - the request will then die silently.
|
||||
*
|
||||
* The exception is if an error occurs and BASE_URL is not yet set - in that case the error is never suppressed.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* $chain = new ErrorControlChain();
|
||||
* $chain->then($callback1)->then($callback2)->then(true, $callback3)->execute();
|
||||
*
|
||||
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
|
||||
* It will likely be heavily refactored before the release of 3.2
|
||||
*/
|
||||
class ErrorControlChain {
|
||||
protected $error = false;
|
||||
protected $steps = array();
|
||||
|
||||
protected $suppression = true;
|
||||
|
||||
/** We can't unregister_shutdown_function, so this acts as a flag to enable handling */
|
||||
protected $handleFatalErrors = false;
|
||||
|
||||
public function hasErrored() {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function setErrored($error) {
|
||||
$this->error = (bool)$error;
|
||||
}
|
||||
|
||||
public function setSuppression($suppression) {
|
||||
$this->suppression = (bool)$suppression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this callback to the chain of callbacks to call along with the state
|
||||
* that $error must be in this point in the chain for the callback to be called
|
||||
*
|
||||
* @param $callback - The callback to call
|
||||
* @param $onErrorState - false if only call if no errors yet, true if only call if already errors, null for either
|
||||
* @return $this
|
||||
*/
|
||||
public function then($callback, $onErrorState = false) {
|
||||
$this->steps[] = array(
|
||||
'callback' => $callback,
|
||||
'onErrorState' => $onErrorState
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function thenWhileGood($callback) {
|
||||
return $this->then($callback, false);
|
||||
}
|
||||
|
||||
public function thenIfErrored($callback) {
|
||||
return $this->then($callback, true);
|
||||
}
|
||||
|
||||
public function thenAlways($callback) {
|
||||
return $this->then($callback, null);
|
||||
}
|
||||
|
||||
public function handleError() {
|
||||
if ($this->suppression && defined('BASE_URL')) throw new Exception('Generic Error');
|
||||
else return false;
|
||||
}
|
||||
|
||||
protected function lastErrorWasFatal() {
|
||||
$error = error_get_last();
|
||||
return $error && $error['type'] == 1;
|
||||
}
|
||||
|
||||
public function handleFatalError() {
|
||||
if ($this->handleFatalErrors && $this->suppression && defined('BASE_URL')) {
|
||||
if ($this->lastErrorWasFatal()) {
|
||||
ob_clean();
|
||||
$this->error = true;
|
||||
$this->step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
set_error_handler(array($this, 'handleError'), error_reporting());
|
||||
register_shutdown_function(array($this, 'handleFatalError'));
|
||||
$this->handleFatalErrors = true;
|
||||
|
||||
$this->step();
|
||||
}
|
||||
|
||||
protected function step() {
|
||||
if ($this->steps) {
|
||||
$step = array_shift($this->steps);
|
||||
|
||||
if ($step['onErrorState'] === null || $step['onErrorState'] === $this->error) {
|
||||
try {
|
||||
call_user_func($step['callback'], $this);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
if ($this->suppression && defined('BASE_URL')) $this->error = true;
|
||||
else throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$this->step();
|
||||
}
|
||||
else {
|
||||
// Now clean up
|
||||
$this->handleFatalErrors = false;
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
}
|
110
core/startup/ParameterConfirmationToken.php
Normal file
110
core/startup/ParameterConfirmationToken.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ParameterConfirmationToken
|
||||
*
|
||||
* When you need to use a dangerous GET parameter that needs to be set before core/Core.php is
|
||||
* established, this class takes care of allowing some other code of confirming the parameter,
|
||||
* by generating a one-time-use token & redirecting with that token included in the redirected URL
|
||||
*
|
||||
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
|
||||
* It will likely be heavily refactored before the release of 3.2
|
||||
*/
|
||||
class ParameterConfirmationToken {
|
||||
protected $parameterName = null;
|
||||
protected $parameter = null;
|
||||
protected $token = null;
|
||||
|
||||
protected function pathForToken($token) {
|
||||
if (defined('BASE_PATH')) {
|
||||
$basepath = BASE_PATH;
|
||||
}
|
||||
else {
|
||||
$basepath = rtrim(dirname(dirname(dirname(dirname(__FILE__)))), DIRECTORY_SEPARATOR);
|
||||
}
|
||||
|
||||
require_once(dirname(dirname(__FILE__)).'/TempPath.php');
|
||||
$tempfolder = getTempFolder($basepath ? $basepath : DIRECTORY_SEPARATOR);
|
||||
|
||||
return $tempfolder.'/token_'.preg_replace('/[^a-z0-9]+/', '', $token);
|
||||
}
|
||||
|
||||
protected function genToken() {
|
||||
// Generate a new random token (as random as possible)
|
||||
require_once(dirname(dirname(dirname(__FILE__))).'/security/RandomGenerator.php');
|
||||
$rg = new RandomGenerator();
|
||||
$token = $rg->randomToken('md5');
|
||||
|
||||
// Store a file in the session save path (safer than /tmp, as open_basedir might limit that)
|
||||
file_put_contents($this->pathForToken($token), $token);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
protected function checkToken($token) {
|
||||
$file = $this->pathForToken($token);
|
||||
$content = null;
|
||||
|
||||
if (file_exists($file)) {
|
||||
$content = file_get_contents($file);
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
return $content == $token;
|
||||
}
|
||||
|
||||
public function __construct($parameterName) {
|
||||
// Store the parameter name
|
||||
$this->parameterName = $parameterName;
|
||||
// Store the parameter value
|
||||
$this->parameter = isset($_GET[$parameterName]) ? $_GET[$parameterName] : null;
|
||||
// Store the token
|
||||
$this->token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null;
|
||||
|
||||
// If a token was provided, but isn't valid, ignore it
|
||||
if ($this->token && (!$this->checkToken($this->token))) $this->token = null;
|
||||
}
|
||||
|
||||
public function parameterProvided() {
|
||||
return $this->parameter !== null;
|
||||
}
|
||||
|
||||
public function tokenProvided() {
|
||||
return $this->token !== null;
|
||||
}
|
||||
|
||||
public function params() {
|
||||
return array(
|
||||
$this->parameterName => $this->parameter,
|
||||
$this->parameterName.'token' => $this->genToken()
|
||||
);
|
||||
}
|
||||
|
||||
public function reloadWithToken() {
|
||||
global $url;
|
||||
|
||||
// Are we http or https?
|
||||
$proto = 'http';
|
||||
|
||||
if(isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
|
||||
if(strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https') $proto = 'https';
|
||||
}
|
||||
|
||||
if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) $proto = 'https';
|
||||
if(isset($_SERVER['SSL'])) $proto = 'https';
|
||||
|
||||
// What's our host
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
|
||||
// What's our GET params (ensuring they include the original parameter + a new token)
|
||||
$params = array_merge($_GET, $this->params());
|
||||
unset($params['url']);
|
||||
|
||||
// Join them all together into the original URL
|
||||
$location = "$proto://" . $host . BASE_URL . $url . ($params ? '?'.http_build_query($params) : '');
|
||||
|
||||
// And redirect
|
||||
header('location: '.$location, true, 302);
|
||||
die;
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ class BehatFixtureFactory extends \FixtureFactory {
|
||||
|
||||
// Copy identifier to some visible property unless its already defined.
|
||||
// Exclude files, since they generate their own named based on the file path.
|
||||
if(!is_a($name, 'File', true)) {
|
||||
if(!$name != 'File' && !is_subclass_of($name, 'File')) {
|
||||
foreach(array('Name', 'Title') as $fieldName) {
|
||||
if(singleton($name)->hasField($fieldName) && !isset($data[$fieldName])) {
|
||||
$data[$fieldName] = $identifier;
|
||||
|
@ -1,5 +1,32 @@
|
||||
# 3.0.6 (Not yet released)
|
||||
|
||||
## Overview
|
||||
|
||||
* Security: Require ADMIN for `?flush=1` (stop denial of service attacks)
|
||||
([#1692](https://github.com/silverstripe/silverstripe-framework/issues/1692))
|
||||
|
||||
## Details
|
||||
|
||||
### Security: Require ADMIN for ?flush=1
|
||||
|
||||
Flushing the various manifests (class, template, config) is performed through a GET
|
||||
parameter (`flush=1`). Since this action requires more server resources than normal requests,
|
||||
it can facilitate [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack).
|
||||
|
||||
To prevent this, main.php now checks and only allows the flush parameter in the following cases:
|
||||
|
||||
* The [environment](/topics/environment-management) is in "dev mode"
|
||||
* A user is logged in with ADMIN permissions
|
||||
* An error occurs during startup
|
||||
|
||||
This applies to both `flush=1` and `flush=all` (technically we only check for the existence of any parameter value)
|
||||
but only through web requests made through main.php - CLI requests, or any other request that goes through
|
||||
a custom start up script will still process all flush requests as normal.
|
||||
|
||||
## Upgrading
|
||||
|
||||
* If you have created your own composite database fields, then you shoulcd amend the setValue() to allow the passing of an object (usually DataObject) as well as an array.
|
||||
* If you have created your own composite database fields, then you should amend the setValue() to allow the passing of
|
||||
an object (usually DataObject) as well as an array.
|
||||
|
||||
* If you have provided your own startup scripts (ones that include core/Core.php) that can be accessed via a web
|
||||
request, you should ensure that you limit use of the flush parameter
|
||||
|
@ -18,6 +18,8 @@
|
||||
|
||||
### Framework
|
||||
|
||||
* Security: Require ADMIN for `?flush=1` (stop denial of service attacks)
|
||||
([#1692](https://github.com/silverstripe/silverstripe-framework/issues/1692))
|
||||
* Static properties are immutable and private, you must use Config API
|
||||
* Statics in custom Page classes need to be "private"
|
||||
* `$default_cast` is now `Text` instead of `HTMLText`, to secure templates from XSS by default
|
||||
@ -37,6 +39,24 @@
|
||||
* Support for [Composer](http://getcomposer.org) dependency manager (also works with 3.0)
|
||||
* Added support for filtering incoming HTML from TinyMCE (disabled by default, see [security](/topics/security))
|
||||
|
||||
## Details
|
||||
|
||||
### Security: Require ADMIN for ?flush=1
|
||||
|
||||
Flushing the various manifests (class, template, config) is performed through a GET
|
||||
parameter (`flush=1`). Since this action requires more server resources than normal requests,
|
||||
it can facilitate [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack).
|
||||
|
||||
To prevent this, main.php now checks and only allows the flush parameter in the following cases:
|
||||
|
||||
* The [environment](/topics/environment-management) is in "dev mode"
|
||||
* A user is logged in with ADMIN permissions
|
||||
* An error occurs during startup
|
||||
|
||||
This applies to both `flush=1` and `flush=all` (technically we only check for the existence of any parameter value)
|
||||
but only through web requests made through main.php - CLI requests, or any other request that goes through
|
||||
a custom start up script will still process all flush requests as normal.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Statics in custom Page classes need to be "private"
|
||||
|
@ -154,7 +154,7 @@ The fields displayed in the edit form are from `DataObject::getCMSFields()`
|
||||
The `GridFieldDetailForm` component drives the record editing form which is usually configured
|
||||
through the configs `GridFieldConfig_RecordEditor` and `GridFieldConfig_RelationEditor`
|
||||
described above. It takes its fields from `DataObject->getCMSFields()`,
|
||||
but can be customized to accept different fields via its `[api:GridFieldDetailForm->setFields()](api:setFields())` method.
|
||||
but can be customized to accept different fields via its `[api:GridFieldDetailForm->setFields()]` method.
|
||||
|
||||
The component also has the ability to load and save data stored on join tables
|
||||
when two records are related via a "many_many" relationship, as defined through
|
||||
|
@ -17,10 +17,8 @@ Append the option and corresponding value to your URL in your browser's address
|
||||
|
||||
| URL Variable | | Values | | Description |
|
||||
| ------------ | | ------ | | ----------- |
|
||||
| flush | | 1,all | | This will clear out all cached information about the page. This is used frequently during development - for example, when adding new PHP or SS files. See below for value descriptions. |
|
||||
| flush=1 | | 1 | | Clears out all caches. Used mainly during development, e.g. when adding new classes or templates. Requires "dev" mode or ADMIN login |
|
||||
| showtemplate | | 1 | | Show the compiled version of all the templates used, including line numbers. Good when you have a syntax error in a template. Cannot be used on a Live site without **isDev**. **flush** can be used with the following values: |
|
||||
| ?flush=1 | | | | Flushes the current page and included templates |
|
||||
| ?flush=all | | | | Flushes the entire template cache |
|
||||
|
||||
## General Testing
|
||||
|
||||
|
26
docs/en/topics/caching.md
Normal file
26
docs/en/topics/caching.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Caching
|
||||
|
||||
## Built-In Caches
|
||||
|
||||
The framework uses caches to store infrequently changing values.
|
||||
By default, the storage mechanism is simply the filesystem, although
|
||||
other cache backends can be configured. All caches use the `[api:SS_Cache]` API.
|
||||
|
||||
The most common caches are manifests of various resources:
|
||||
|
||||
* PHP class locations (`[api:SS_ClassManifest]`)
|
||||
* Template file locations and compiled templates (`[api:SS_TemplateManifest]`)
|
||||
* Configuration settings from YAML files (`[api:SS_ConfigManifest]`)
|
||||
* Language files (`[api:i18n]`)
|
||||
|
||||
Flushing the various manifests is performed through a GET
|
||||
parameter (`flush=1`). Since this action requires more server resources than normal requests,
|
||||
executing the action is limited to the following cases when performed via a web request:
|
||||
|
||||
* The [environment](/topics/environment-management) is in "dev mode"
|
||||
* A user is logged in with ADMIN permissions
|
||||
* An error occurs during startup
|
||||
|
||||
## Custom Caches
|
||||
|
||||
See `[api:SS_Cache]`.
|
@ -4,6 +4,7 @@ This section provides an overview on how things fit together, the "conceptual gl
|
||||
It is where most documentation should live, and is the natural "second step" after finishing the tutorials.
|
||||
|
||||
* [Access Control and Page Security](access-control): Restricting access and setting up permissions on your website
|
||||
* [Caching](caching): Explains built-in caches for classes, config and templates. How to use your own caches.
|
||||
* [Command line Usage](commandline): Calling controllers via the command line interface using `sake`
|
||||
* [Configuration](configuration): Influence behaviour through PHP and YAML configuration
|
||||
* [Controller](controller): The intermediate layer between your templates and the data model
|
||||
|
@ -127,6 +127,38 @@ or [sanitize](http://htmlpurifier.org/) it correctly.
|
||||
See [http://shiflett.org/articles/foiling-cross-site-attacks](http://shiflett.org/articles/foiling-cross-site-attacks)
|
||||
for in-depth information about "Cross-Site-Scripting".
|
||||
|
||||
### What if I can't trust my editors?
|
||||
|
||||
The default configuration of SilverStripe assumes some level of trust is given to your editors who have access
|
||||
to the CMS. Though the HTML WYSIWYG editor is configured to provide some control over the HTML an editor provides,
|
||||
this is not enforced server side, and so can be bypassed by a malicious editor. A editor that does so can use an
|
||||
XSS attack against an admin to perform any administrative action.
|
||||
|
||||
If you can't trust your editors, SilverStripe must be configured to filter the content so that any javascript is
|
||||
stripped out
|
||||
|
||||
To enable filtering, set the HtmlEditorField::$sanitise_server_side [configuration](/topics/configuration) property to
|
||||
true, e.g.
|
||||
|
||||
HtmlEditorField::config()->sanitise_server_side = true
|
||||
|
||||
The built in sanitiser enforces the TinyMCE whitelist rules on the server side, and is sufficient to eliminate the
|
||||
most common XSS vectors.
|
||||
|
||||
However some subtle XSS attacks that exploit HTML parsing bugs need heavier filtering. For greater protection
|
||||
you can install the [htmlpurifier](https://github.com/silverstripe-labs/silverstripe-htmlpurifier) module which
|
||||
will replace the built in sanitiser with one that uses the [HTML Purifier](http://htmlpurifier.org/) library.
|
||||
|
||||
In both cases, you must ensure that you have not configured TinyMCE to explicitly allow script elements or other
|
||||
javascript-specific attributes.
|
||||
|
||||
##### But I also need my editors to provide javascript
|
||||
|
||||
It is not currently possible to allow editors to provide javascript content and yet still protect other users
|
||||
from any malicious code within that javascript.
|
||||
|
||||
We recommend configuring [shortcodes](/reference/shortcodes) that can be used by editors in place of using javascript directly.
|
||||
|
||||
### Escaping model properties
|
||||
|
||||
`[api:SSViewer]` (the SilverStripe template engine) automatically takes care of escaping HTML tags from specific
|
||||
@ -377,11 +409,62 @@ you need to serve directly.
|
||||
|
||||
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details
|
||||
specific to your web server
|
||||
See [Apache](/installation/webserver) and [Nginx](/installation/nginx) installation documentation for details specific to your web server
|
||||
|
||||
## Passwords
|
||||
|
||||
SilverStripe stores passwords with a strong hashing algorithm (blowfish) by default
|
||||
(see [api:PasswordEncryptor]). It adds randomness to these hashes via
|
||||
salt values generated with the strongest entropy generators available on the platform
|
||||
(see [api:RandomGenerator]). This prevents brute force attacks with
|
||||
[Rainbow tables](http://en.wikipedia.org/wiki/Rainbow_table).
|
||||
|
||||
Strong passwords are a crucial part of any system security.
|
||||
So in addition to storing the password in a secure fashion,
|
||||
you can also enforce specific password policies by configuring
|
||||
a [api:PasswordValidator]:
|
||||
|
||||
:::php
|
||||
$validator = new PasswordValidator();
|
||||
$validator->minLength(7);
|
||||
$validator->checkHistoricalPasswords(6);
|
||||
$validator->characterStrength('lowercase','uppercase','digits','punctuation');
|
||||
Member::set_password_validator($validator);
|
||||
|
||||
In addition, you can tighten password security with the following configuration settings:
|
||||
|
||||
* `Member.password_expiry_days`: Set the number of days that a password should be valid for.
|
||||
* `Member.lock_out_after_incorrect_logins`: Number of incorrect logins after which
|
||||
the user is blocked from further attempts for the timespan defined in `$lock_out_delay_mins`
|
||||
* `Member.lock_out_delay_mins`: Minutes of enforced lockout after incorrect password attempts.
|
||||
Only applies if `lock_out_after_incorrect_logins` is greater than 0.
|
||||
|
||||
## Clickjacking: Prevent iframe Inclusion
|
||||
|
||||
"[Clickjacking](http://en.wikipedia.org/wiki/Clickjacking)" is a malicious technique
|
||||
where a web user is tricked into clicking on hidden interface elements, which can
|
||||
lead to the attacker gaining access to user data or taking control of the website behaviour.
|
||||
|
||||
You can signal to browsers that the current response isn't allowed to be
|
||||
included in HTML "frame" or "iframe" elements, and thereby prevent the most common
|
||||
attack vector. This is done through a HTTP header, which is usually added in your
|
||||
controller's `init()` method:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
This is a recommended option to secure any controller which displays
|
||||
or submits sensitive user input, and is enabled by default in all CMS controllers,
|
||||
as well as the login form.
|
||||
|
||||
## Related
|
||||
|
||||
* [http://silverstripe.org/security-releases/](http://silverstripe.org/security-releases/)
|
||||
|
||||
## Links
|
||||
|
||||
* [Best-practices for securing MySQL (securityfocus.com)](http://www.securityfocus.com/infocus/1726)
|
||||
|
@ -1195,6 +1195,7 @@ class UploadField extends FileField {
|
||||
*
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return SS_HTTPResponse
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function upload(SS_HTTPRequest $request) {
|
||||
if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
|
||||
@ -1222,6 +1223,7 @@ class UploadField extends FileField {
|
||||
// Format response with json
|
||||
$response = new SS_HTTPResponse(Convert::raw2json(array($return)));
|
||||
$response->addHeader('Content-Type', 'text/plain');
|
||||
if(!empty($return['error'])) $response->setStatusCode(403);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
@ -367,6 +367,7 @@ en:
|
||||
EMPTYNEWPASSWORD: 'The new password can''t be empty, please try again'
|
||||
ENTEREMAIL: 'Please enter an email address to get a password reset link.'
|
||||
ERRORLOCKEDOUT: 'Your account has been temporarily disabled because of too many failed attempts at logging in. Please try again in 20 minutes.'
|
||||
ERRORLOCKEDOUT2: 'Your account has been temporarily disabled because of too many failed attempts at logging in. Please try again in {count} minutes.'
|
||||
ERRORNEWPASSWORD: 'You have entered your new password differently, try again'
|
||||
ERRORPASSWORDNOTMATCH: 'Your current password does not match, please try again'
|
||||
ERRORWRONGCRED: 'That doesn''t seem to be the right e-mail address or password. Please try again.'
|
||||
|
149
main.php
149
main.php
@ -56,52 +56,113 @@ if (version_compare(phpversion(), '5.3.2', '<')) {
|
||||
/**
|
||||
* Include SilverStripe's core code
|
||||
*/
|
||||
require_once('core/Core.php');
|
||||
require_once('core/startup/ErrorControlChain.php');
|
||||
require_once('core/startup/ParameterConfirmationToken.php');
|
||||
|
||||
// IIS will sometimes generate this.
|
||||
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
|
||||
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
|
||||
}
|
||||
$chain = new ErrorControlChain();
|
||||
$token = new ParameterConfirmationToken('flush');
|
||||
|
||||
// PHP 5.4's built-in webserver uses this
|
||||
if (php_sapi_name() == 'cli-server') {
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
// Querystring args need to be explicitly parsed
|
||||
if(strpos($url,'?') !== false) {
|
||||
list($url, $query) = explode('?',$url,2);
|
||||
parse_str($query, $_GET);
|
||||
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
|
||||
}
|
||||
|
||||
// Pass back to the webserver for files that exist
|
||||
if(file_exists(BASE_PATH . $url)) return false;
|
||||
$chain
|
||||
// First, if $_GET['flush'] was set, but no valid token, suppress the flush
|
||||
->then(function($chain) use ($token){
|
||||
if (isset($_GET['flush']) && !$token->tokenProvided()) {
|
||||
unset($_GET['flush']);
|
||||
}
|
||||
else {
|
||||
$chain->setSuppression(false);
|
||||
}
|
||||
})
|
||||
// Then load in core
|
||||
->then(function(){
|
||||
require_once('core/Core.php');
|
||||
})
|
||||
// Then build the URL (even if Core didn't load beyond setting BASE_URL)
|
||||
->thenAlways(function(){
|
||||
global $url;
|
||||
|
||||
// Apache rewrite rules use this
|
||||
} else if (isset($_GET['url'])) {
|
||||
$url = $_GET['url'];
|
||||
// IIS includes get variables in url
|
||||
$i = strpos($url, '?');
|
||||
if($i !== false) {
|
||||
$url = substr($url, 0, $i);
|
||||
}
|
||||
|
||||
// Lighttpd uses this
|
||||
} else {
|
||||
if(strpos($_SERVER['REQUEST_URI'],'?') !== false) {
|
||||
list($url, $query) = explode('?', $_SERVER['REQUEST_URI'], 2);
|
||||
parse_str($query, $_GET);
|
||||
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
|
||||
} else {
|
||||
$url = $_SERVER["REQUEST_URI"];
|
||||
}
|
||||
}
|
||||
// IIS will sometimes generate this.
|
||||
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
|
||||
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
|
||||
}
|
||||
|
||||
// Remove base folders from the URL if webroot is hosted in a subfolder
|
||||
if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url = substr($url, strlen(BASE_URL));
|
||||
// PHP 5.4's built-in webserver uses this
|
||||
if (php_sapi_name() == 'cli-server') {
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
|
||||
// Connect to database
|
||||
require_once('model/DB.php');
|
||||
// Querystring args need to be explicitly parsed
|
||||
if(strpos($url,'?') !== false) {
|
||||
list($url, $query) = explode('?',$url,2);
|
||||
parse_str($query, $_GET);
|
||||
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
|
||||
}
|
||||
|
||||
// Apache rewrite rules use this
|
||||
} else if (isset($_GET['url'])) {
|
||||
$url = $_GET['url'];
|
||||
// IIS includes get variables in url
|
||||
$i = strpos($url, '?');
|
||||
if($i !== false) {
|
||||
$url = substr($url, 0, $i);
|
||||
}
|
||||
|
||||
// Lighttpd uses this
|
||||
} else {
|
||||
if(strpos($_SERVER['REQUEST_URI'],'?') !== false) {
|
||||
list($url, $query) = explode('?', $_SERVER['REQUEST_URI'], 2);
|
||||
parse_str($query, $_GET);
|
||||
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
|
||||
} else {
|
||||
$url = $_SERVER["REQUEST_URI"];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove base folders from the URL if webroot is hosted in a subfolder
|
||||
if (substr(strtolower($url), 0, strlen(BASE_URL)) == strtolower(BASE_URL)) $url = substr($url, strlen(BASE_URL));
|
||||
})
|
||||
// Then start up the database
|
||||
->then(function(){
|
||||
require_once('model/DB.php');
|
||||
global $databaseConfig;
|
||||
if ($databaseConfig) DB::connect($databaseConfig);
|
||||
})
|
||||
// Then if a flush was requested, redirect to it
|
||||
->then(function($chain) use ($token){
|
||||
if ($token->parameterProvided() && !$token->tokenProvided()) {
|
||||
// First, check if we're in dev mode, or the database doesn't have any security data
|
||||
$canFlush = Director::isDev() || !Security::database_is_ready();
|
||||
|
||||
// Otherwise, we start up the session if needed, then check for admin
|
||||
if (!$canFlush) {
|
||||
if(!isset($_SESSION) && (isset($_COOKIE[session_name()]) || isset($_REQUEST[session_name()]))) {
|
||||
Session::start();
|
||||
}
|
||||
|
||||
if (Permission::check('ADMIN')) {
|
||||
$canFlush = true;
|
||||
}
|
||||
else {
|
||||
$loginPage = Director::absoluteURL(Config::inst()->get('Security', 'login_url'));
|
||||
$loginPage .= "?BackURL=" . urlencode($_SERVER['REQUEST_URI']);
|
||||
|
||||
header('location: '.$loginPage, true, 302);
|
||||
die;
|
||||
}
|
||||
}
|
||||
|
||||
// And if we can flush, reload with an authority token
|
||||
if ($canFlush) $token->reloadWithToken();
|
||||
}
|
||||
})
|
||||
// Finally if a flush was requested but there was an error while figuring out if it's allowed, do it anyway
|
||||
->thenIfErrored(function() use ($token){
|
||||
if ($token->parameterProvided() && !$token->tokenProvided()) {
|
||||
$token->reloadWithToken();
|
||||
}
|
||||
})
|
||||
->execute();
|
||||
|
||||
// If we're in PHP's built in webserver, pass back to the webserver for files that exist
|
||||
if (php_sapi_name() == 'cli-server' && file_exists(BASE_PATH . $url) && is_file(BASE_PATH . $url)) return false;
|
||||
|
||||
global $databaseConfig;
|
||||
|
||||
@ -112,17 +173,15 @@ if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseC
|
||||
}
|
||||
$s = (isset($_SERVER['SSL']) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) ? 's' : '';
|
||||
$installURL = "http$s://" . $_SERVER['HTTP_HOST'] . BASE_URL . '/install.php';
|
||||
|
||||
|
||||
// The above dirname() will equate to "\" on Windows when installing directly from http://localhost (not using
|
||||
// a sub-directory), this really messes things up in some browsers. Let's get rid of the backslashes
|
||||
$installURL = str_replace('\\', '', $installURL);
|
||||
|
||||
|
||||
header("Location: $installURL");
|
||||
die();
|
||||
}
|
||||
|
||||
DB::connect($databaseConfig);
|
||||
|
||||
// Direct away - this is the "main" function, that hands control to the appropriate controller
|
||||
DataModel::set_inst(new DataModel());
|
||||
Director::direct($url, DataModel::inst());
|
||||
|
@ -29,6 +29,8 @@
|
||||
* NOTE: The cache logic uses tags, and so a backend that supports tags is required. Currently only the File
|
||||
* backend (and the two-level backend with the File backend as the slow store) meets this requirement
|
||||
*
|
||||
* @deprecated 3.1 Use DataList to aggregate data
|
||||
*
|
||||
* @author hfried
|
||||
* @package framework
|
||||
* @subpackage core
|
||||
@ -60,10 +62,15 @@ class Aggregate extends ViewableData {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @deprecated 3.1 Use DataList to aggregate data
|
||||
*
|
||||
* @param string $type The DataObject type we are building an aggregate for
|
||||
* @param string $filter (optional) An SQL filter to apply to the selected rows before calculating the aggregate
|
||||
*/
|
||||
public function __construct($type, $filter = '') {
|
||||
Deprecation::notice('3.1', 'Call aggregate methods on a DataList directly instead. In templates'
|
||||
. ' an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead'
|
||||
. ' (check partial-caching.md documentation for more details.)');
|
||||
$this->type = $type;
|
||||
$this->filter = $filter;
|
||||
parent::__construct();
|
||||
|
@ -1468,39 +1468,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query object for a $has_many Component.
|
||||
*
|
||||
* @param string $componentName
|
||||
* @param string $filter
|
||||
* @param string|array $sort
|
||||
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
|
||||
* @param string|array $limit
|
||||
* @return SQLQuery
|
||||
* @deprecated 3.1 Use getComponents to get a filtered DataList for an object's relation
|
||||
*/
|
||||
public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
|
||||
if(!$componentClass = $this->has_many($componentName)) {
|
||||
user_error("DataObject::getComponentsQuery(): Unknown 1-to-many component '$componentName'"
|
||||
. " on class '$this->class'", E_USER_ERROR);
|
||||
}
|
||||
|
||||
if($join) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
|
||||
);
|
||||
}
|
||||
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many');
|
||||
|
||||
$id = $this->getField("ID");
|
||||
|
||||
// get filter
|
||||
$combinedFilter = "\"$joinField\" = '$id'";
|
||||
if(!empty($filter)) $combinedFilter .= " AND ({$filter})";
|
||||
|
||||
return DataList::create($componentClass)
|
||||
->where($combinedFilter)
|
||||
->canSortBy($sort)
|
||||
->limit($limit);
|
||||
Deprecation::notice('3.1', "Use getComponents to get a filtered DataList for an object's relation");
|
||||
return $this->getComponents($componentName, $filter, $sort, $join, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@
|
||||
* @uses DropdownField
|
||||
*
|
||||
* @param string $name
|
||||
* @param DataOject $object The object that the foreign key is stored on (should have a relation with $name)
|
||||
* @param DataObject $object The object that the foreign key is stored on (should have a relation with $name)
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
|
@ -113,9 +113,18 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var Int
|
||||
* @var Int Number of incorrect logins after which
|
||||
* the user is blocked from further attempts for the timespan
|
||||
* defined in {@link $lock_out_delay_mins}.
|
||||
*/
|
||||
private static $lock_out_after_incorrect_logins = null;
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var integer Minutes of enforced lockout after incorrect password attempts.
|
||||
* Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
|
||||
*/
|
||||
private static $lock_out_delay_mins = 15;
|
||||
|
||||
/**
|
||||
* @config
|
||||
@ -210,6 +219,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
public function checkPassword($password) {
|
||||
$result = $this->canLogIn();
|
||||
|
||||
// Short-circuit the result upon failure, no further checks needed.
|
||||
if (!$result->valid()) return $result;
|
||||
|
||||
if(empty($this->Password) && $this->exists()) {
|
||||
$result->error(_t('Member.NoPassword','There is no password on this member.'));
|
||||
return $result;
|
||||
@ -238,11 +250,15 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$result = ValidationResult::create();
|
||||
|
||||
if($this->isLockedOut()) {
|
||||
$result->error(_t (
|
||||
'Member.ERRORLOCKEDOUT',
|
||||
'Your account has been temporarily disabled because of too many failed attempts at ' .
|
||||
'logging in. Please try again in 20 minutes.'
|
||||
));
|
||||
$result->error(
|
||||
_t(
|
||||
'Member.ERRORLOCKEDOUT2',
|
||||
'Your account has been temporarily disabled because of too many failed attempts at ' .
|
||||
'logging in. Please try again in {count} minutes.',
|
||||
null,
|
||||
array('count' => $this->config()->lock_out_delay_mins)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->extend('canLogIn', $result);
|
||||
@ -429,7 +445,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
self::session_regenerate_id();
|
||||
Session::set("loggedInAs", $member->ID);
|
||||
// This lets apache rules detect whether the user has logged in
|
||||
if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
|
||||
if(Member::config()->login_marker_cookie) {
|
||||
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
|
||||
}
|
||||
|
||||
$generator = new RandomGenerator();
|
||||
$token = $generator->randomToken('sha1');
|
||||
@ -717,7 +735,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$encryption_details = Security::encrypt_password(
|
||||
$this->Password, // this is assumed to be cleartext
|
||||
$this->Salt,
|
||||
($this->PasswordEncryption) ? $this->PasswordEncryption : Security::config()->password_encryption_algorithm,
|
||||
($this->PasswordEncryption) ?
|
||||
$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
|
||||
$this
|
||||
);
|
||||
|
||||
@ -1407,7 +1426,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$this->write();
|
||||
|
||||
if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
|
||||
$this->LockedOutUntil = date('Y-m-d H:i:s', time() + 15*60);
|
||||
$lockoutMins = self::config()->lock_out_delay_mins;
|
||||
$this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60);
|
||||
$this->write();
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +270,13 @@ class Security extends Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
|
||||
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the login form to process according to the submitted data
|
||||
|
@ -8,17 +8,17 @@
|
||||
|
||||
<body>
|
||||
<div class="mainblock" style="width: 290px;">
|
||||
<% if UseSimpleForm %>
|
||||
<% if $UseSimpleForm %>
|
||||
$EditImageSimpleForm
|
||||
<% else %>
|
||||
$EditImageForm
|
||||
<% end_if %>
|
||||
</div>
|
||||
|
||||
<% if Image.ID %>
|
||||
<% if $Image.ID %>
|
||||
<div class="mainblock" >
|
||||
$Image.CMSThumbnail
|
||||
<% if DeleteImageForm %>
|
||||
<% if $DeleteImageForm %>
|
||||
$DeleteImageForm
|
||||
<% end_if %>
|
||||
</div>
|
||||
@ -26,4 +26,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -1,27 +1,27 @@
|
||||
<% if IncludeFormTag %>
|
||||
<% if $IncludeFormTag %>
|
||||
<form $AttributesHTML>
|
||||
<% end_if %>
|
||||
<% if Message %>
|
||||
<% if $Message %>
|
||||
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
|
||||
<% else %>
|
||||
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
|
||||
<% end_if %>
|
||||
|
||||
<fieldset>
|
||||
<% if Legend %><legend>$Legend</legend><% end_if %>
|
||||
<% loop Fields %>
|
||||
<% if $Legend %><legend>$Legend</legend><% end_if %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
<div class="clear"><!-- --></div>
|
||||
</fieldset>
|
||||
|
||||
<% if Actions %>
|
||||
<% if $Actions %>
|
||||
<div class="Actions">
|
||||
<% loop Actions %>
|
||||
<% loop $Actions %>
|
||||
$Field
|
||||
<% end_loop %>
|
||||
</div>
|
||||
<% end_if %>
|
||||
<% if IncludeFormTag %>
|
||||
<% if $IncludeFormTag %>
|
||||
</form>
|
||||
<% end_if %>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="add-existing-autocompleter">
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<span>$Field</span>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<tr class="filter-header" style="display:none;">
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<th class="extra">$Field</th>
|
||||
<% end_loop %>
|
||||
</tr>
|
||||
|
@ -7,8 +7,8 @@
|
||||
$NumRecords
|
||||
</span>
|
||||
|
||||
<% if Message %>
|
||||
<% if $Message %>
|
||||
<div class="datagrid-footer-message">$Message</div>
|
||||
<% end_if %>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<% if Backlink %>
|
||||
<% if $Backlink %>
|
||||
<a href="$Backlink"><% _t('Go back', 'Go back' ) %></a>
|
||||
<% end_if %>
|
||||
|
||||
$ItemEditForm
|
||||
$ItemEditForm
|
||||
|
@ -1,5 +1,5 @@
|
||||
<tr class="sortable-header">
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<th class="main col-$getName">$Field</th>
|
||||
<% end_loop %>
|
||||
</tr>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<tr class="title">
|
||||
<th colspan="$ColumnCount">
|
||||
<% if Title %><h2>$Title</h2><% end_if %>
|
||||
<% if $Title %><h2>$Title</h2><% end_if %>
|
||||
<div class="right">\$DefineFragment(toolbar-header-right)</div>
|
||||
<div class="left">\$DefineFragment(toolbar-header-left)</div>
|
||||
</th>
|
||||
</tr>
|
||||
</tr>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<button $AttributesHTML>
|
||||
<% if ButtonContent %>$ButtonContent<% else %>$Title<% end_if %>
|
||||
</button>
|
||||
<% if $ButtonContent %>$ButtonContent<% else %>$Title<% end_if %>
|
||||
</button>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<tr class="ss-gridfield-item ss-gridfield-{$EvenOdd} $FirstLast" data-id="$ID">
|
||||
<% if $GridField.ExtraColumnsCount %>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<td>$Value</td>
|
||||
<% end_loop %>
|
||||
<td colspan="$GridField.ExtraColumnsCount" class="ss-gridfield-last"></td>
|
||||
<% else %>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
<td <% if FirstLast %>class="ss-gridfield-{$FirstLast}"<% end_if %>>$Value</td>
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
|
@ -7,12 +7,12 @@
|
||||
<% if $Title %><h3>$Title</h3><% end_if %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><% loop Header %><th>$CellString</th><% end_loop %></tr>
|
||||
<tr><% loop $Header %><th>$CellString</th><% end_loop %></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if ItemRows %>
|
||||
<% loop ItemRows %>
|
||||
<tr><% loop ItemRow %><td>$CellString</td><% end_loop %></tr>
|
||||
<% if $ItemRows %>
|
||||
<% loop $ItemRows %>
|
||||
<tr><% loop $ItemRow %><td>$CellString</td><% end_loop %></tr>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
<tr>
|
||||
|
@ -38,10 +38,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if Info %><div class="info">$Info</div><% end_if %>
|
||||
<% if $Info %><div class="info">$Info</div><% end_if %>
|
||||
<div class="details ss-uploadfield-item-editform loading">
|
||||
<fieldset>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
</fieldset>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<% if canEdit %>
|
||||
<% if $canEdit %>
|
||||
<button class="ss-uploadfield-item-edit ss-ui-button ui-corner-all" title="<% _t('UploadField.EDITINFO', 'Edit this file') %>" data-icon="pencil">
|
||||
<% _t('UploadField.EDIT', 'Edit') %>
|
||||
<span class="toggle-details">
|
||||
@ -8,10 +8,10 @@
|
||||
<% end_if %>
|
||||
<button class="ss-uploadfield-item-remove ss-ui-button ui-corner-all" title="<% _t('UploadField.REMOVEINFO', 'Remove this file from here, but do not delete it from the file store') %>" data-icon="plug-disconnect-prohibition">
|
||||
<% _t('UploadField.REMOVE', 'Remove') %></button>
|
||||
<% if canDelete %>
|
||||
<% if $canDelete %>
|
||||
<button data-href="$UploadFieldDeleteLink" class="ss-uploadfield-item-delete ss-ui-button ui-corner-all" title="<% _t('UploadField.DELETEINFO', 'Permanently delete this file from the file store') %>" data-icon="minus-circle"><% _t('UploadField.DELETE', 'Delete from files') %></button>
|
||||
<% end_if %>
|
||||
<% if UploadField.canAttachExisting %>
|
||||
<% if $UploadField.canAttachExisting %>
|
||||
<button class="ss-uploadfield-item-choose-another ss-uploadfield-fromfiles ss-ui-button ui-corner-all" title="<% _t('UploadField.CHOOSEANOTHERINFO', 'Replace this file with another one from the file store') %>" data-icon="network-cloud">
|
||||
<% _t('UploadField.CHOOSEANOTHERFILE', 'Choose another file') %></button>
|
||||
<% end_if %>
|
||||
|
@ -6,14 +6,14 @@
|
||||
<atom:link href="$Link" rel="self" type="application/rss+xml" />
|
||||
<description>$Description.XML</description>
|
||||
|
||||
<% loop Entries %>
|
||||
<% loop $Entries %>
|
||||
<item>
|
||||
<title>$Title.XML</title>
|
||||
<link>$AbsoluteLink</link>
|
||||
<% if Description %><description>$Description.AbsoluteLinks.XML</description><% end_if %>
|
||||
<% if Date %><pubDate>$Date.Rfc822</pubDate>
|
||||
<% if $Description %><description>$Description.AbsoluteLinks.XML</description><% end_if %>
|
||||
<% if $Date %><pubDate>$Date.Rfc822</pubDate>
|
||||
<% else %><pubDate>$Created.Rfc822</pubDate><% end_if %>
|
||||
<% if Author %><dc:creator>$Author.XML</dc:creator><% end_if %>
|
||||
<% if $Author %><dc:creator>$Author.XML</dc:creator><% end_if %>
|
||||
<guid>$AbsoluteLink</guid>
|
||||
</item>
|
||||
<% end_loop %>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p id="$Name" class="field $Type">
|
||||
$Field
|
||||
<label class="right" for="$id">$Title</label>
|
||||
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
</p>
|
||||
<% if $Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
</p>
|
||||
|
@ -1,15 +1,15 @@
|
||||
<form $FormAttributes>
|
||||
<% if Message %>
|
||||
<% if $Message %>
|
||||
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
|
||||
<% else %>
|
||||
<p id="{$FormName}_error" class="message $MessageType" style="display: none"></p>
|
||||
<% end_if %>
|
||||
<fieldset>
|
||||
<% loop Fields %>
|
||||
$FieldHolder
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
<% loop Actions %>
|
||||
$Field
|
||||
<% loop $Actions %>
|
||||
$Field
|
||||
<% end_loop %>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<div class="clear"><!-- --></div>
|
||||
</label>
|
||||
<div class="ss-uploadfield-item-actions">
|
||||
<% if Top.isActive %>
|
||||
<% if $Top.isActive %>
|
||||
$UploadFieldFileButtons
|
||||
<% end_if %>
|
||||
</div>
|
||||
@ -25,7 +25,7 @@
|
||||
<% end_loop %>
|
||||
<% end_if %>
|
||||
</ul>
|
||||
<% if canUpload || canAttachExisting %>
|
||||
<% if $canUpload || $canAttachExisting %>
|
||||
<div class="ss-uploadfield-item ss-uploadfield-addfile<% if $CustomisedItems %> borderTop<% end_if %>">
|
||||
<% if canUpload %>
|
||||
<div class="ss-uploadfield-item-preview ss-uploadfield-dropzone ui-corner-all">
|
||||
@ -43,21 +43,21 @@
|
||||
<% else %>
|
||||
<b><% _t('UploadField.ATTACHFILE', 'Attach a file') %></b>
|
||||
<% end_if %>
|
||||
<% if canPreviewFolder %>
|
||||
<% if $canPreviewFolder %>
|
||||
<small>(<%t UploadField.UPLOADSINTO 'saves into /{path}' path=$FolderName %>)</small>
|
||||
<% end_if %>
|
||||
</label>
|
||||
<% if canUpload %>
|
||||
<% if $canUpload %>
|
||||
<label class="ss-uploadfield-fromcomputer ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Upload from your computer') %>" data-icon="drive-upload">
|
||||
<% _t('UploadField.FROMCOMPUTER', 'From your computer') %>
|
||||
<input id="$id" name="{$Name}[Uploads][]" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="$configString" type="file"<% if $multiple %> multiple="multiple"<% end_if %> />
|
||||
</label>
|
||||
<% end_if %>
|
||||
|
||||
<% if canAttachExisting %>
|
||||
<% if $canAttachExisting %>
|
||||
<button class="ss-uploadfield-fromfiles ss-ui-button ui-corner-all" title="<% _t('UploadField.FROMCOMPUTERINFO', 'Select from files') %>" data-icon="network-cloud"><% _t('UploadField.FROMFILES', 'From files') %></button>
|
||||
<% end_if %>
|
||||
<% if canUpload %>
|
||||
<% if $canUpload %>
|
||||
<% if not $autoUpload %>
|
||||
<button class="ss-uploadfield-startall ss-ui-button ui-corner-all" data-icon="navigation"><% _t('UploadField.STARTALL', 'Start all') %></button>
|
||||
<% end_if %>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div id="$HolderID" class="field<% if extraClass %> $extraClass<% end_if %>">
|
||||
$Field
|
||||
<label class="right" for="$ID">$Title</label>
|
||||
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
<% if Description %><span class="description">$Description</span><% end_if %>
|
||||
<% if $Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
<% if $Description %><span class="description">$Description</span><% end_if %>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
$Field
|
||||
|
||||
<% if $Title %>
|
||||
<label class="checkboxfield-small" <% if ID %>for="$ID"<% end_if %>>$Title</label>
|
||||
<% end_if %>
|
||||
<label class="checkboxfield-small" <% if $ID %>for="$ID"<% end_if %>>$Title</label>
|
||||
<% end_if %>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<ul id="$HolderID" class="$extraClass">
|
||||
<% if Options.Count %>
|
||||
<% loop Options %>
|
||||
<% if $Options.Count %>
|
||||
<% loop $Options %>
|
||||
<li class="$Class">
|
||||
<input id="$ID" class="checkbox" name="$Name" type="checkbox" value="$Value"<% if isChecked %> checked="checked"<% end_if %><% if isDisabled %> disabled="disabled"<% end_if %> />
|
||||
<input id="$ID" class="checkbox" name="$Name" type="checkbox" value="$Value"<% if $isChecked %> checked="checked"<% end_if %><% if $isDisabled %> disabled="disabled"<% end_if %> />
|
||||
<label for="$ID">$Title</label>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<legend>$Legend</legend>
|
||||
<% end_if %>
|
||||
|
||||
<% loop FieldList %>
|
||||
<% if ColumnCount %>
|
||||
<% loop $FieldList %>
|
||||
<% if $ColumnCount %>
|
||||
<div class="column-{$ColumnCount} $FirstLast">
|
||||
$Field
|
||||
</div>
|
||||
@ -12,4 +12,4 @@
|
||||
$Field
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
</$Tag>
|
||||
</$Tag>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<legend>$Legend</legend>
|
||||
<% end_if %>
|
||||
|
||||
<% loop FieldList %>
|
||||
<% if ColumnCount %>
|
||||
<% loop $FieldList %>
|
||||
<% if $ColumnCount %>
|
||||
<div class="column-{$ColumnCount} $FirstLast">
|
||||
$FieldHolder
|
||||
</div>
|
||||
@ -13,5 +13,5 @@
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
|
||||
<% if Description %><span class="description">$Description</span><% end_if %>
|
||||
</$Tag>
|
||||
<% if $Description %><span class="description">$Description</span><% end_if %>
|
||||
</$Tag>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<select $AttributesHTML>
|
||||
<% loop Options %>
|
||||
<option value="$Value.XML"<% if Selected %> selected="selected"<% end_if %><% if Disabled %> disabled="disabled"<% end_if %>>$Title.XML</option>
|
||||
<% loop $Options %>
|
||||
<option value="$Value.XML"<% if $Selected %> selected="selected"<% end_if %><% if $Disabled %> disabled="disabled"<% end_if %>>$Title.XML</option>
|
||||
<% end_loop %>
|
||||
</select>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="<% if extraClass %>$extraClass<% else %>fieldgroup<% end_if %><% if Zebra %> fieldgroup-zebra<% end_if %>" <% if ID %>id="$ID"<% end_if %>>
|
||||
<% loop FieldList %>
|
||||
<div class="<% if $extraClass %>$extraClass<% else %>fieldgroup<% end_if %><% if $Zebra %> fieldgroup-zebra<% end_if %>" <% if $ID %>id="$ID"<% end_if %>>
|
||||
<% loop $FieldList %>
|
||||
<div class="fieldgroup-field $FirstLast $EvenOdd">
|
||||
$SmallFieldHolder
|
||||
</div>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="<% if extraClass %>$extraClass<% else %>fieldgroup<% end_if %><% if Zebra %> fieldgroup-zebra<% end_if %>" <% if ID %>id="$ID"<% end_if %>>
|
||||
<% loop FieldList %>
|
||||
<div class="<% if $extraClass %>$extraClass<% else %>fieldgroup<% end_if %><% if $Zebra %> fieldgroup-zebra<% end_if %>" <% if ID %>id="$ID"<% end_if %>>
|
||||
<% loop $FieldList %>
|
||||
<div class="fieldgroup-field $FirstLast $EvenOdd">
|
||||
$FieldHolder
|
||||
</div>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<div <% if Name %>id="$Name"<% end_if %> class="field $Type $extraClass">
|
||||
<% if Title %><label class="left">$Title</label><% end_if %>
|
||||
<div <% if $Name %>id="$Name"<% end_if %> class="field $Type $extraClass">
|
||||
<% if $Title %><label class="left">$Title</label><% end_if %>
|
||||
|
||||
<div class="middleColumn fieldgroup <% if Zebra %>fieldgroup-$Zebra<% end_if %>">
|
||||
<% loop FieldList %>
|
||||
<% loop $FieldList %>
|
||||
<div class="fieldgroup-field $FirstLast $EvenOdd">
|
||||
$SmallFieldHolder
|
||||
</div>
|
||||
<% end_loop %>
|
||||
</div>
|
||||
<% if RightTitle %><label class="right">$RightTitle</label><% end_if %>
|
||||
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
</div>
|
||||
<% if $RightTitle %><label class="right">$RightTitle</label><% end_if %>
|
||||
<% if $Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<% if UseButtonTag %>
|
||||
<% if $UseButtonTag %>
|
||||
<button $AttributesHTML>
|
||||
<% if ButtonContent %>$ButtonContent<% else %>$Title<% end_if %>
|
||||
<% if $ButtonContent %>$ButtonContent<% else %>$Title<% end_if %>
|
||||
</button>
|
||||
<% else %>
|
||||
<input $AttributesHTML />
|
||||
<% end_if %>
|
||||
<% end_if %>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<% if isReadonly %>
|
||||
<span id="$ID"<% if extraClass %> class="$extraClass"<% end_if %>>
|
||||
<% if $isReadonly %>
|
||||
<span id="$ID"<% if $extraClass %> class="$extraClass"<% end_if %>>
|
||||
$Value
|
||||
</span>
|
||||
<% else %>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<div id="$HolderID" class="field<% if extraClass %> $extraClass<% end_if %>">
|
||||
<% if Title %><label class="left" for="$ID">$Title</label><% end_if %>
|
||||
<div id="$HolderID" class="field<% if $extraClass %> $extraClass<% end_if %>">
|
||||
<% if $Title %><label class="left" for="$ID">$Title</label><% end_if %>
|
||||
<div class="middleColumn">
|
||||
$Field
|
||||
</div>
|
||||
<% if RightTitle %><label class="right" for="$ID">$RightTitle</label><% end_if %>
|
||||
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
<% if Description %><span class="description">$Description</span><% end_if %>
|
||||
</div>
|
||||
<% if $RightTitle %><label class="right" for="$ID">$RightTitle</label><% end_if %>
|
||||
<% if $Message %><span class="message $MessageType">$Message</span><% end_if %>
|
||||
<% if $Description %><span class="description">$Description</span><% end_if %>
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<div class="fieldholder-small">
|
||||
<% if $RightTitle %>
|
||||
<label class="right fieldholder-small-label" <% if ID %>for="$ID"<% end_if %>>$RightTitle</label>
|
||||
<label class="right fieldholder-small-label" <% if $ID %>for="$ID"<% end_if %>>$RightTitle</label>
|
||||
<% else_if $LeftTitle %>
|
||||
<label class="left fieldholder-small-label" <% if ID %>for="$ID"<% end_if %>>$LeftTitle</label>
|
||||
<label class="left fieldholder-small-label" <% if $ID %>for="$ID"<% end_if %>>$LeftTitle</label>
|
||||
<% else_if $Title %>
|
||||
<label class="fieldholder-small-label" <% if ID %>for="$ID"<% end_if %>>$Title</label>
|
||||
<label class="fieldholder-small-label" <% if $ID %>for="$ID"<% end_if %>>$Title</label>
|
||||
<% end_if %>
|
||||
|
||||
$Field
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ul id="$HolderID" class="$extraClass">
|
||||
<% loop Options %>
|
||||
<% loop $Options %>
|
||||
<li class="$Class">
|
||||
<input id="$ID" class="radio" name="$Name" type="radio" value="$Value"<% if isChecked %> checked<% end_if %><% if isDisabled %> disabled<% end_if %> />
|
||||
<input id="$ID" class="radio" name="$Name" type="radio" value="$Value"<% if $isChecked %> checked<% end_if %><% if $isDisabled %> disabled<% end_if %> />
|
||||
<label for="$ID">$Title</label>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
|
@ -1,14 +1,20 @@
|
||||
<% if IsReadonly %>
|
||||
<% if $IsReadonly %>
|
||||
<ul class="SelectionGroup<% if extraClass %> $extraClass<% end_if %>">
|
||||
<% loop FieldSet %>
|
||||
<% if Selected %>
|
||||
<li$Selected>
|
||||
$RadioLabel
|
||||
$FieldHolder
|
||||
</li>
|
||||
<% loop $FieldSet %>
|
||||
<% if $Selected %>
|
||||
<li$Selected>
|
||||
$RadioLabel
|
||||
$FieldHolder
|
||||
</li>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
<% end_loop %>
|
||||
<% else %>
|
||||
<ul class="SelectionGroup<% if extraClass %> $extraClass<% end_if %>"><% loop FieldSet %><li$Selected>{$RadioButton}{$RadioLabel}{$FieldHolder}</li><% end_loop %></ul>
|
||||
<ul class="SelectionGroup<% if extraClass %> $extraClass<% end_if %>">
|
||||
<% loop $FieldSet %>
|
||||
<li$Selected>
|
||||
{$RadioButton}{$RadioLabel}{$FieldHolder}
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<div $getAttributesHTML("class") class="ss-tabset $extraClass">
|
||||
<ul>
|
||||
<% loop Tabs %>
|
||||
<% loop $Tabs %>
|
||||
<li class="$FirstLast $MiddleString $extraClass"><a href="#$id" id="tab-$id">$Title</a></li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
|
||||
<% loop Tabs %>
|
||||
<% if Tabs %>
|
||||
<% loop $Tabs %>
|
||||
<% if $Tabs %>
|
||||
$FieldHolder
|
||||
<% else %>
|
||||
<div $AttributesHTML>
|
||||
<% loop Fields %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
class="TreeDropdownField single<% if extraClass %> $extraClass<% end_if %><% if ShowSearch %> searchable<% end_if %>"
|
||||
data-url-tree="$Link(tree)"
|
||||
data-title="$TitleURLEncoded"
|
||||
<% if Description %>title="$Description"<% end_if %>
|
||||
<% if Metadata %>data-metadata="$Metadata"<% end_if %>>
|
||||
<% if $Description %>title="$Description"<% end_if %>
|
||||
<% if $Metadata %>data-metadata="$Metadata"<% end_if %>>
|
||||
<input id="$ID" type="hidden" name="$Name" value="$Value" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,12 +172,14 @@ class RestfulServiceTest extends SapphireTest {
|
||||
public function testHttpHeaderParseing() {
|
||||
$headers = "content-type: text/html; charset=UTF-8\r\n".
|
||||
"Server: Funky/1.0\r\n".
|
||||
"X-BB-ExampleMANycaPS: test\r\n".
|
||||
"Set-Cookie: foo=bar\r\n".
|
||||
"Set-Cookie: baz=quux\r\n".
|
||||
"Set-Cookie: bar=foo\r\n";
|
||||
$expected = array(
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
'Server' => 'Funky/1.0',
|
||||
'X-BB-ExampleMANycaPS' => 'test',
|
||||
'Set-Cookie' => array(
|
||||
'foo=bar',
|
||||
'baz=quux',
|
||||
|
90
tests/core/startup/ErrorControlChainTest.php
Normal file
90
tests/core/startup/ErrorControlChainTest.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
class ErrorControlChainTest extends SapphireTest {
|
||||
|
||||
function testErrorSuppression() {
|
||||
$chain = new ErrorControlChain();
|
||||
|
||||
$chain
|
||||
->then(function(){
|
||||
user_error('This error should be suppressed', E_USER_ERROR);
|
||||
})
|
||||
->execute();
|
||||
|
||||
$this->assertTrue($chain->hasErrored());
|
||||
}
|
||||
|
||||
function testMultipleErrorSuppression() {
|
||||
$chain = new ErrorControlChain();
|
||||
|
||||
$chain
|
||||
->then(function(){
|
||||
user_error('This error should be suppressed', E_USER_ERROR);
|
||||
})
|
||||
->thenAlways(function(){
|
||||
user_error('This error should also be suppressed', E_USER_ERROR);
|
||||
})
|
||||
->execute();
|
||||
|
||||
$this->assertTrue($chain->hasErrored());
|
||||
}
|
||||
|
||||
function testExceptionSuppression() {
|
||||
$chain = new ErrorControlChain();
|
||||
|
||||
$chain
|
||||
->then(function(){
|
||||
throw new Exception('This exception should be suppressed');
|
||||
})
|
||||
->execute();
|
||||
|
||||
$this->assertTrue($chain->hasErrored());
|
||||
}
|
||||
|
||||
function testMultipleExceptionSuppression() {
|
||||
$chain = new ErrorControlChain();
|
||||
|
||||
$chain
|
||||
->then(function(){
|
||||
throw new Exception('This exception should be suppressed');
|
||||
})
|
||||
->thenAlways(function(){
|
||||
throw new Exception('This exception should also be suppressed');
|
||||
})
|
||||
->execute();
|
||||
|
||||
$this->assertTrue($chain->hasErrored());
|
||||
}
|
||||
|
||||
function testErrorControl() {
|
||||
$preError = $postError = array('then' => false, 'thenIfErrored' => false, 'thenAlways' => false);
|
||||
|
||||
$chain = new ErrorControlChain();
|
||||
|
||||
$chain
|
||||
->then(function() use (&$preError) { $preError['then'] = true; })
|
||||
->thenIfErrored(function() use (&$preError) { $preError['thenIfErrored'] = true; })
|
||||
->thenAlways(function() use (&$preError) { $preError['thenAlways'] = true; })
|
||||
|
||||
->then(function(){ user_error('An error', E_USER_ERROR); })
|
||||
|
||||
->then(function() use (&$postError) { $postError['then'] = true; })
|
||||
->thenIfErrored(function() use (&$postError) { $postError['thenIfErrored'] = true; })
|
||||
->thenAlways(function() use (&$postError) { $postError['thenAlways'] = true; })
|
||||
|
||||
->execute();
|
||||
|
||||
$this->assertEquals(
|
||||
array('then' => true, 'thenIfErrored' => false, 'thenAlways' => true),
|
||||
$preError,
|
||||
'Then and thenAlways callbacks called before error, thenIfErrored callback not called'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array('then' => false, 'thenIfErrored' => true, 'thenAlways' => true),
|
||||
$postError,
|
||||
'thenIfErrored and thenAlways callbacks called after error, then callback not called'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -153,6 +153,33 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$this->assertEquals($relationCount + 1, $record->ManyManyFiles()->Count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Partially covered by {@link UploadTest->testUploadAcceptsAllowedExtension()},
|
||||
* but this test additionally verifies that those constraints are actually enforced
|
||||
* in this controller method.
|
||||
*/
|
||||
public function testAllowedExtensions() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$invalidFile = 'invalid.php';
|
||||
$_FILES = array('AllowedExtensionsField' => $this->getUploadFile($invalidFile));
|
||||
$response = $this->post(
|
||||
'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload',
|
||||
array('AllowedExtensionsField' => $this->getUploadFile($invalidFile))
|
||||
);
|
||||
$this->assertTrue($response->isError());
|
||||
$this->assertContains('Extension is not allowed', $response->getBody());
|
||||
|
||||
$validFile = 'valid.txt';
|
||||
$_FILES = array('AllowedExtensionsField' => $this->getUploadFile($validFile));
|
||||
$response = $this->post(
|
||||
'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload',
|
||||
array('AllowedExtensionsField' => $this->getUploadFile($validFile))
|
||||
);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertNotContains('Extension is not allowed', $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that has_one relations do not support multiple files
|
||||
*/
|
||||
@ -899,6 +926,9 @@ class UploadFieldTestForm extends Form implements TestOnly {
|
||||
$fieldCanAttachExisting = UploadField::create('CanAttachExistingFalseField')
|
||||
->setCanAttachExisting(false);
|
||||
|
||||
$fieldAllowedExtensions = new UploadField('AllowedExtensionsField');
|
||||
$fieldAllowedExtensions->getValidator()->setAllowedExtensions(array('txt'));
|
||||
|
||||
$fields = new FieldList(
|
||||
$fieldNoRelation,
|
||||
$fieldHasOne,
|
||||
@ -913,7 +943,8 @@ class UploadFieldTestForm extends Form implements TestOnly {
|
||||
$fieldDisabled,
|
||||
$fieldSubfolder,
|
||||
$fieldCanUploadFalse,
|
||||
$fieldCanAttachExisting
|
||||
$fieldCanAttachExisting,
|
||||
$fieldAllowedExtensions
|
||||
);
|
||||
$actions = new FieldList(
|
||||
new FormAction('submit')
|
||||
@ -943,4 +974,4 @@ class UploadFieldTest_Controller extends Controller implements TestOnly {
|
||||
public function Form() {
|
||||
return new UploadFieldTestForm($this, 'Form');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -250,61 +250,76 @@ class SecurityTest extends FunctionalTest {
|
||||
i18n::set_locale('en_US');
|
||||
|
||||
Member::config()->lock_out_after_incorrect_logins = 5;
|
||||
Member::config()->lock_out_delay_mins = 15;
|
||||
|
||||
/* LOG IN WITH A BAD PASSWORD 7 TIMES */
|
||||
|
||||
for($i=1;$i<=7;$i++) {
|
||||
// Login with a wrong password for more than the defined threshold
|
||||
for($i = 1; $i <= Member::config()->lock_out_after_incorrect_logins+1; $i++) {
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
$member = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'test'));
|
||||
|
||||
/* THE FIRST 4 TIMES, THE MEMBER SHOULDN'T BE LOCKED OUT */
|
||||
if($i < 5) {
|
||||
$this->assertNull($member->LockedOutUntil);
|
||||
if($i < Member::config()->lock_out_after_incorrect_logins) {
|
||||
$this->assertNull(
|
||||
$member->LockedOutUntil,
|
||||
'User does not have a lockout time set if under threshold for failed attempts'
|
||||
);
|
||||
$this->assertContains($this->loginErrorMessage(), _t('Member.ERRORWRONGCRED'));
|
||||
} else {
|
||||
// Fuzzy matching for time to avoid side effects from slow running tests
|
||||
$this->assertGreaterThan(
|
||||
time() + 14*60,
|
||||
strtotime($member->LockedOutUntil),
|
||||
'User has a lockout time set after too many failed attempts'
|
||||
);
|
||||
}
|
||||
|
||||
/* AFTER THAT THE USER IS LOCKED OUT FOR 15 MINUTES */
|
||||
|
||||
//(we check for at least 14 minutes because we don't want a slow running test to report a failure.)
|
||||
else {
|
||||
$this->assertGreaterThan(time() + 14*60, strtotime($member->LockedOutUntil));
|
||||
}
|
||||
|
||||
if($i > 5) {
|
||||
$this->assertContains(_t('Member.ERRORLOCKEDOUT'), $this->loginErrorMessage());
|
||||
// $this->assertTrue(false !== stripos($this->loginErrorMessage(), _t('Member.ERRORLOCKEDOUT')));
|
||||
$msg = _t(
|
||||
'Member.ERRORLOCKEDOUT2',
|
||||
'Your account has been temporarily disabled because of too many failed attempts at ' .
|
||||
'logging in. Please try again in {count} minutes.',
|
||||
null,
|
||||
array('count' => Member::config()->lock_out_delay_mins)
|
||||
);
|
||||
if($i > Member::config()->lock_out_after_incorrect_logins) {
|
||||
$this->assertContains($msg, $this->loginErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* THE USER CAN'T LOG IN NOW, EVEN IF THEY GET THE RIGHT PASSWORD */
|
||||
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , '1nitialPassword');
|
||||
$this->assertNull($this->session()->inst_get('loggedInAs'));
|
||||
$this->assertNull(
|
||||
$this->session()->inst_get('loggedInAs'),
|
||||
'The user can\'t log in after being locked out, even with the right password'
|
||||
);
|
||||
|
||||
/* BUT, IF TIME PASSES, THEY CAN LOG IN */
|
||||
|
||||
// (We fake this by re-setting LockedOutUntil)
|
||||
$member = DataObject::get_by_id("Member", $this->idFromFixture('Member', 'test'));
|
||||
$member->LockedOutUntil = date('Y-m-d H:i:s', time() - 30);
|
||||
$member->write();
|
||||
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , '1nitialPassword');
|
||||
$this->assertEquals($this->session()->inst_get('loggedInAs'), $member->ID);
|
||||
$this->assertEquals(
|
||||
$this->session()->inst_get('loggedInAs'),
|
||||
$member->ID,
|
||||
'After lockout expires, the user can login again'
|
||||
);
|
||||
|
||||
// Log the user out
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
|
||||
/* NOW THAT THE LOCK-OUT HAS EXPIRED, CHECK THAT WE ARE ALLOWED 4 FAILED ATTEMPTS BEFORE LOGGING IN */
|
||||
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
// Login again with wrong password, but less attempts than threshold
|
||||
for($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , 'incorrectpassword');
|
||||
}
|
||||
$this->assertNull($this->session()->inst_get('loggedInAs'));
|
||||
$this->assertTrue(false !== stripos($this->loginErrorMessage(), _t('Member.ERRORWRONGCRED')));
|
||||
$this->assertTrue(
|
||||
false !== stripos($this->loginErrorMessage(), _t('Member.ERRORWRONGCRED')),
|
||||
'The user can retry with a wrong password after the lockout expires'
|
||||
);
|
||||
|
||||
$this->doTestLoginForm('sam@silverstripe.com' , '1nitialPassword');
|
||||
$this->assertEquals($this->session()->inst_get('loggedInAs'), $member->ID);
|
||||
$this->assertEquals(
|
||||
$this->session()->inst_get('loggedInAs'),
|
||||
$member->ID,
|
||||
'The user can login successfully after lockout expires, if staying below the threshold'
|
||||
);
|
||||
|
||||
i18n::set_locale($local);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user