diff --git a/_config/requestprocessors.yml b/_config/requestprocessors.yml
new file mode 100644
index 000000000..1e9310bd3
--- /dev/null
+++ b/_config/requestprocessors.yml
@@ -0,0 +1,8 @@
+---
+Name: requestprocessors
+---
+Injector:
+ RequestProcessor:
+ properties:
+ filters:
+ - '%$VersionedRequestFilter'
diff --git a/admin/code/AdminRootController.php b/admin/code/AdminRootController.php
index 765500c03..77a394ce6 100644
--- a/admin/code/AdminRootController.php
+++ b/admin/code/AdminRootController.php
@@ -1,5 +1,9 @@
*
- * @package cms
- * @subpackage batchaction
+ * @package framework
+ * @subpackage admin
*/
abstract class CMSBatchAction extends Object {
diff --git a/admin/code/CMSBatchActionHandler.php b/admin/code/CMSBatchActionHandler.php
index d3229e694..a8854e662 100644
--- a/admin/code/CMSBatchActionHandler.php
+++ b/admin/code/CMSBatchActionHandler.php
@@ -3,8 +3,8 @@
/**
* Special request handler for admin/batchaction
*
- * @package cms
- * @subpackage batchaction
+ * @package framework
+ * @subpackage admin
*/
class CMSBatchActionHandler extends RequestHandler {
diff --git a/admin/code/CMSForm.php b/admin/code/CMSForm.php
index a03d51178..39641a95c 100644
--- a/admin/code/CMSForm.php
+++ b/admin/code/CMSForm.php
@@ -1,6 +1,11 @@
setCurrentPageID(Member::currentUserID());
$form = parent::getEditForm($id, $fields);
- if($form instanceof SS_HTTPResponse) return $form;
+ if($form instanceof SS_HTTPResponse) {
+ return $form;
+ }
+
$form->Fields()->removeByName('LastVisited');
$form->Fields()->push(new HiddenField('ID', null, Member::currentUserID()));
$form->Actions()->push(
@@ -32,11 +40,21 @@ class CMSProfileController extends LeftAndMain {
->setAttribute('data-icon', 'accept')
->setUseButtonTag(true)
);
+
$form->Actions()->removeByName('action_delete');
- $form->setValidator(new Member_Validator());
$form->setTemplate('Form');
$form->setAttribute('data-pjax-fragment', null);
- if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
+
+ if($member = Member::currentUser()) {
+ $form->setValidator($member->getValidator());
+ } else {
+ $form->setValidator(Injector::inst()->get('Member')->getValidator());
+ }
+
+ if($form->Fields()->hasTabset()) {
+ $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
+ }
+
$form->addExtraClass('member-profile-form root-form cms-edit-form cms-panel-padded center');
return $form;
diff --git a/admin/code/GroupImportForm.php b/admin/code/GroupImportForm.php
index e4271f002..a544e8373 100644
--- a/admin/code/GroupImportForm.php
+++ b/admin/code/GroupImportForm.php
@@ -1,10 +1,11 @@
getRecord($id);
- if($record && !$record->canView()) return Security::permissionFailure($this);
+
+ if($record && !$record->canView()) {
+ return Security::permissionFailure($this);
+ }
$memberList = GridField::create(
'Members',
@@ -70,7 +75,16 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
->addComponent(new GridFieldButtonRow('after'))
->addComponent(new GridFieldExportButton('buttons-after-left'))
)->addExtraClass("members_grid");
- $memberListConfig->getComponentByType('GridFieldDetailForm')->setValidator(new Member_Validator());
+
+ if($record && method_exists($record, 'getValidator')) {
+ $validator = $record->getValidator();
+ } else {
+ $validator = Injector::inst()->get('Member')->getValidator();
+ }
+
+ $memberListConfig
+ ->getComponentByType('GridFieldDetailForm')
+ ->setValidator($validator);
$groupList = GridField::create(
'Groups',
diff --git a/admin/css/ie7.css b/admin/css/ie7.css
index 0375b8db6..d783e44af 100644
--- a/admin/css/ie7.css
+++ b/admin/css/ie7.css
@@ -20,8 +20,8 @@
.ss-gridfield-button-filter.ss-ui-button.hover-alike { background-color: #338DC1; background-position: -16px 6px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button { background: #e6e6e6 url(../images/filter-icons.png) no-repeat 8px 5px; filter: none; }
-.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: red url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
-.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #e60000 url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
+.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: #d81b21 url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
+.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #c1181e url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
.cms table.ss-gridfield-table tr td { border-right: 1px solid #9a9a9a; }
.cms table.ss-gridfield-table tr th { border-right: 1px solid #9a9a9a; }
diff --git a/admin/css/ie8.css b/admin/css/ie8.css
index 569c11ab2..b2253cb85 100644
--- a/admin/css/ie8.css
+++ b/admin/css/ie8.css
@@ -20,8 +20,8 @@
.ss-gridfield-button-filter.ss-ui-button.hover-alike { background-color: #338DC1; background-position: -16px 6px; filter: none; }
.ss-gridfield-button-reset.ss-ui-button { background: #e6e6e6 url(../images/filter-icons.png) no-repeat 8px 5px; filter: none; }
-.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: red url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
-.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #e60000 url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
+.ss-gridfield-button-reset.ss-ui-button.filtered:hover { background: #d81b21 url(../images/filter-icons.png) no-repeat 8px -17px; filter: none; }
+.ss-gridfield-button-reset.ss-ui-button.filtered:active { background: #c1181e url(../images/filter-icons.png) no-repeat 9px -16px; filter: none; }
.cms table.ss-gridfield-table tr td { border-right: 1px solid #9a9a9a; }
.cms table.ss-gridfield-table tr th { border-right: 1px solid #9a9a9a; }
diff --git a/admin/css/screen.css b/admin/css/screen.css
index 522e24a62..140fde63a 100644
--- a/admin/css/screen.css
+++ b/admin/css/screen.css
@@ -217,7 +217,9 @@ form.small .field input.text, form.small .field textarea, form.small .field sele
.cms .ss-ui-button.ss-ui-action-constructive { text-shadow: none; font-weight: bold; color: white; border-color: #1f9433; border-bottom-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzkzYmU0MiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzFmOTQzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #93be42), color-stop(100%, #1f9433)); background: -webkit-linear-gradient(#93be42, #1f9433); background: -moz-linear-gradient(#93be42, #1f9433); background: -o-linear-gradient(#93be42, #1f9433); background: linear-gradient(#93be42, #1f9433); text-shadow: #1c872f 0 -1px -1px; }
.cms .ss-ui-button.ss-ui-action-constructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-constructive:hover { border-color: #166a24; background-color: #1f9433; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E0Y2EzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzIzYTkzYSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a4ca3a), color-stop(100%, #23a93a)); background: -webkit-linear-gradient(#a4ca3a, #23a93a); background: -moz-linear-gradient(#a4ca3a, #23a93a); background: -o-linear-gradient(#a4ca3a, #23a93a); background: linear-gradient(#a4ca3a, #23a93a); }
.cms .ss-ui-button.ss-ui-action-constructive:active, .cms .ss-ui-button.ss-ui-action-constructive:focus, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-constructive.ui-state-focus { background-color: #1d8c30; -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); }
-.cms .ss-ui-button.ss-ui-action-destructive { color: red; background-color: #e6e6e6; }
+.cms .ss-ui-button.ss-ui-action-destructive { color: #fff; text-shadow: none; border-color: #980c10; border-bottom-color: #69080b; background-color: #d81b21; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2YzM2Y0NCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2Q4MWIyMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f33f44), color-stop(100%, #d81b21)); background: -webkit-linear-gradient(#f33f44, #d81b21); background: -moz-linear-gradient(#f33f44, #d81b21); background: -o-linear-gradient(#f33f44, #d81b21); background: linear-gradient(#f33f44, #d81b21); text-shadow: #ca191f 0 -1px -1px; }
+.cms .ss-ui-button.ss-ui-action-destructive.ui-state-hover, .cms .ss-ui-button.ss-ui-action-destructive:hover { border-color: #69080b; background-color: #d81b21; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Y5MzQzYSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U0MjgyZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f9343a), color-stop(100%, #e4282e)); background: -webkit-linear-gradient(#f9343a, #e4282e); background: -moz-linear-gradient(#f9343a, #e4282e); background: -o-linear-gradient(#f9343a, #e4282e); background: linear-gradient(#f9343a, #e4282e); }
+.cms .ss-ui-button.ss-ui-action-destructive:active, .cms .ss-ui-button.ss-ui-action-destructive:focus, .cms .ss-ui-button.ss-ui-action-destructive.ui-state-active, .cms .ss-ui-button.ss-ui-action-destructive.ui-state-focus { background-color: #cf1a20; -webkit-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); -moz-box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); box-shadow: inset 0 1px 3px #17181a, 0 1px 0 rgba(255, 255, 255, 0.6); }
.cms .ss-ui-button.ss-ui-button-small .ui-button-text { font-size: 10px; }
.cms .ss-ui-button.ui-state-highlight { background-color: #e6e6e6; border: 1px solid #708284; }
.cms .ss-ui-button.ss-ui-action-minor { background: none; border: 0; color: #393939; text-decoration: underline; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; }
@@ -437,6 +439,7 @@ body.cms { overflow: hidden; }
/** -------------------------------------------- Actions -------------------------------------------- */
.cms-content-actions, .cms-preview-controls { margin: 0; padding: 12px 12px; z-index: 0; border-top: 1px solid #cacacc; -webkit-box-shadow: 1px 0 0 #eceff1, rgba(248, 248, 248, 0.9) 0 1px 0px inset, rgba(201, 205, 206, 0.8) 0 0 1px; -moz-box-shadow: 1px 0 0 #eceff1, rgba(248, 248, 248, 0.9) 0 1px 0px inset, rgba(201, 205, 206, 0.8) 0 0 1px; box-shadow: 1px 0 0 #eceff1, rgba(248, 248, 248, 0.9) 0 1px 0px inset, rgba(201, 205, 206, 0.8) 0 0 1px; height: 28px; background-color: #eceff1; }
+.cms-content-actions .ss-ui-action-destructive, .cms-preview-controls .ss-ui-action-destructive { float: right; margin-left: 8px; }
/** -------------------------------------------- Messages -------------------------------------------- */
.message { display: block; clear: both; margin: 0 0 8px; padding: 10px 12px; font-weight: normal; border: 1px #ccc solid; background: #fff; background: rgba(255, 255, 255, 0.5); text-shadow: none; -webkit-border-radius: 3px 3px 3px 3px; -moz-border-radius: 3px 3px 3px 3px; -ms-border-radius: 3px 3px 3px 3px; -o-border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px; }
@@ -513,10 +516,10 @@ body.cms { overflow: hidden; }
.cms-content-tools .field { /* Fields are more compressed in the sidebar compared to the main content editing window so the below alters the internal spacing of the fields so we can move that spacing to between the form fields rather than padding */ }
.cms-content-tools .field label { float: none; width: auto; font-size: 11px; padding: 0 8px 4px 0; }
.cms-content-tools .field .middleColumn { margin: 0; }
-.cms-content-tools .field .description { margin-left: 0; }
.cms-content-tools .field input.text, .cms-content-tools .field select, .cms-content-tools .field textarea { padding: 5px; font-size: 11px; }
.cms-content-tools .field.checkbox { padding: 0 0 8px; }
.cms-content-tools .field.checkbox input { margin: 2px 0; }
+.cms-content-tools .field .description { margin-left: 0; }
.cms-content-tools .fieldgroup .fieldgroup-field { padding: 0; }
.cms-content-tools .fieldgroup .fieldgroup-field .field { margin: 0; padding: 0; }
.cms-content-tools table { margin: 8px -4px; }
diff --git a/admin/scss/_forms.scss b/admin/scss/_forms.scss
index 1c479ef20..9f17487ca 100644
--- a/admin/scss/_forms.scss
+++ b/admin/scss/_forms.scss
@@ -434,8 +434,34 @@ form.small .field, .field.small {
/* destructive */
&.ss-ui-action-destructive {
- color: $color-button-destructive;
- background-color: $color-button-generic;
+ color: #fff;
+ text-shadow:none;
+ border-color: $color-button-destructive-border;
+ border-bottom-color: darken($color-button-destructive-border, 10%);
+ background-color: $color-button-destructive;
+ @include background(
+ linear-gradient(color-stops(
+ scale-color(lighten($color-button-destructive, 10%), $red:50%),
+ $color-button-destructive
+ ))
+ );
+ @include text-shadow(darken($color-button-destructive, 3%) 0 -1px -1px);
+
+ &.ui-state-hover, &:hover {
+ border-color: darken($color-button-destructive-border, 10%);
+ background-color: $color-button-destructive;
+ @include background(
+ linear-gradient(color-stops(
+ scale-color(saturate(lighten($color-button-destructive, 10%), 10%), $red:60%),
+ lighten($color-button-destructive, 5%)
+ ))
+ );
+ }
+
+ &:active, &:focus, &.ui-state-active, &.ui-state-focus {
+ background-color: darken($color-button-destructive, 2%);
+ @include box-shadow(inset 0 1px 3px rgb(23, 24, 26), 0 1px 0 rgba(255, 255, 255, .6));
+ }
}
&.ss-ui-button-small {
diff --git a/admin/scss/_style.scss b/admin/scss/_style.scss
index 943b9b6f1..bd8a85353 100644
--- a/admin/scss/_style.scss
+++ b/admin/scss/_style.scss
@@ -426,6 +426,11 @@ body.cms {
$color-shadow-light 0 0 1px);
height: 28px;
background-color: $tab-panel-texture-color;
+
+ .ss-ui-action-destructive {
+ float: right;
+ margin-left: 8px;
+ }
}
diff --git a/admin/scss/themes/_default.scss b/admin/scss/themes/_default.scss
index 53a2e6308..b1d860e23 100644
--- a/admin/scss/themes/_default.scss
+++ b/admin/scss/themes/_default.scss
@@ -56,7 +56,8 @@ $color-button-highlight-border: #708284 !default;
$color-button-constructive: #1F9433 !default;
$color-button-constructive-border: #1F9433 !default;
-$color-button-destructive: #f00 !default;
+$color-button-destructive: #d81b21 !default;
+$color-button-destructive-border: #980c10 !default;
$color-button-disabled: #eeeded !default;
diff --git a/control/Session.php b/control/Session.php
index a4aaf0b89..0610d4fb1 100644
--- a/control/Session.php
+++ b/control/Session.php
@@ -614,6 +614,6 @@ class Session {
*/
public static function get_timeout() {
Deprecation::notice('3.2', 'Use the "Session.timeout" config setting instead');
- return Config::inst()->update('Session', 'timeout');
+ return Config::inst()->get('Session', 'timeout');
}
}
diff --git a/control/VersionedRequestFilter.php b/control/VersionedRequestFilter.php
new file mode 100644
index 000000000..c6158cc4f
--- /dev/null
+++ b/control/VersionedRequestFilter.php
@@ -0,0 +1,17 @@
+has_extension($class, $requiredExtension))
+ * @param string $classOrExtension if 1 argument supplied, the class name of the extension to check for; if 2 supplied, the class name to test
+ * @param string $requiredExtension used only if 2 arguments supplied
*/
- public static function has_extension($requiredExtension) {
- $class = get_called_class();
+ public static function has_extension($classOrExtension, $requiredExtension = null) {
+ //BC support
+ if(func_num_args() > 1){
+ $class = $classOrExtension;
+ $requiredExtension = $requiredExtension;
+ }
+ else {
+ $class = get_called_class();
+ $requiredExtension = $classOrExtension;
+ }
$requiredExtension = strtolower($requiredExtension);
$extensions = Config::inst()->get($class, 'extensions');
diff --git a/css/GridField.css b/css/GridField.css
index 43ab08fd1..0a0b8c62b 100644
--- a/css/GridField.css
+++ b/css/GridField.css
@@ -30,10 +30,10 @@ Used in side panels and action tabs
.cms .ss-gridfield .ss-gridfield-buttonrow { font-size: 14.4px; }
.cms .ss-gridfield .grid-levelup { text-indent: -9999em; margin-bottom: 6px; }
.cms .ss-gridfield .grid-levelup a.list-parent-link { background: transparent url(../images/gridfield-level-up.png) no-repeat 0 0; display: block; }
-.cms .ss-gridfield .add-existing-autocompleter { width: 500px; }
-.cms .ss-gridfield .add-existing-autocompleter span { display: -moz-inline-stack; display: inline-block; vertical-align: top; *vertical-align: auto; zoom: 1; *display: inline; }
-.cms .ss-gridfield .add-existing-autocompleter input.relation-search { width: 270px; margin-bottom: 12px; }
-.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { font-size: 12px; margin-bottom: 0; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
+.cms .ss-gridfield .add-existing-autocompleter span { float: left; display: -moz-inline-stack; display: inline-block; vertical-align: top; *vertical-align: auto; zoom: 1; *display: inline; }
+.cms .ss-gridfield .add-existing-autocompleter input.relation-search { width: 270px; height: 32px; margin-bottom: 12px; border-top-right-radius: 0; border-bottom-right-radius: 0; }
+.cms .ss-gridfield .add-existing-autocompleter button#action_gridfield_relationadd { height: 32px; margin-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; border-left: none; }
+.cms .ss-gridfield .grid-csv-button, .cms .ss-gridfield .grid-print-button { margin-bottom: 0; font-size: 12px; display: -moz-inline-stack; display: inline-block; vertical-align: middle; *vertical-align: auto; zoom: 1; *display: inline; }
.cms table.ss-gridfield-table { display: table; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; padding: 0; border-collapse: separate; border-bottom: 0 none; width: 100%; }
.cms table.ss-gridfield-table thead { color: #323e46; background: transparent; }
.cms table.ss-gridfield-table thead tr.filter-header .fieldgroup { max-width: 512px; }
diff --git a/dev/Debug.php b/dev/Debug.php
index 1d3f98f07..c17d68c82 100644
--- a/dev/Debug.php
+++ b/dev/Debug.php
@@ -23,19 +23,7 @@
* @subpackage dev
*/
class Debug {
-
- /**
- * @config
- * @var string Email address to send error notifications
- */
- private static $send_errors_to;
-
- /**
- * @config
- * @var string Email address to send warning notifications
- */
- private static $send_warnings_to;
-
+
/**
* @config
* @var String indicating the file where errors are logged.
@@ -262,18 +250,6 @@ class Debug {
if(error_reporting() == 0) return;
ini_set('display_errors', 0);
- if(Config::inst()->get('Debug', 'send_warnings_to')) {
- return self::emailError(
- Config::inst()->get('Debug', 'send_warnings_to'),
- $errno,
- $errstr,
- $errfile,
- $errline,
- $errcontext,
- "Warning"
- );
- }
-
// Send out the error details to the logger for writing
SS_Log::log(
array(
@@ -286,10 +262,6 @@ class Debug {
SS_Log::WARN
);
- if(Config::inst()->get('Debug', 'log_errors_to')) {
- self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Warning");
- }
-
if(Director::isDev()) {
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Warning");
} else {
@@ -310,13 +282,6 @@ class Debug {
*/
public static function fatalHandler($errno, $errstr, $errfile, $errline, $errcontext) {
ini_set('display_errors', 0);
-
- if(Config::inst()->get('Debug', 'send_errors_to')) {
- self::emailError(
- Config::inst()->get('Debug', 'send_errors_to'), $errno,
- $errstr, $errfile, $errline, $errcontext, "Error"
- );
- }
// Send out the error details to the logger for writing
SS_Log::log(
@@ -330,10 +295,6 @@ class Debug {
SS_Log::ERR
);
- if(Config::inst()->get('Debug', 'log_errors_to')) {
- self::log_error_if_necessary( $errno, $errstr, $errfile, $errline, $errcontext, "Error");
- }
-
if(Director::isDev() || Director::is_cli()) {
return self::showError($errno, $errstr, $errfile, $errline, $errcontext, "Error");
} else {
diff --git a/dev/install/install.php5 b/dev/install/install.php5
index 02cf81343..f1419f2ae 100755
--- a/dev/install/install.php5
+++ b/dev/install/install.php5
@@ -653,7 +653,7 @@ class InstallRequirements {
// special case for display_errors, check the original value before
// it was changed at the start of this script.
- if($settingName = 'display_errors') {
+ if($settingName == 'display_errors') {
global $originalDisplayErrorsValue;
$val = $originalDisplayErrorsValue;
} else {
diff --git a/docs/en/topics/i18n.md b/docs/en/topics/i18n.md
index 79041d528..3f9ef98d5 100644
--- a/docs/en/topics/i18n.md
+++ b/docs/en/topics/i18n.md
@@ -212,6 +212,17 @@ the PHP version of the function.
// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>
+
+#### Caching in Template Files with locale switching
+
+When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key otherwise it won't pick up locale changes.
+
+ ::::ss
+ <% cached 'MyIdentifier', $CurrentLocale %>
+ <% loop $Students %>
+ $Name
+ <% end_loop %>
+ <% end_cached %>
## Collecting text
diff --git a/email/Email.php b/email/Email.php
index 6aa8c4811..e3645434a 100644
--- a/email/Email.php
+++ b/email/Email.php
@@ -181,6 +181,7 @@ class Email extends ViewableData {
'filename' => $filename,
'mimetype' => $mimetype,
);
+ return $this;
}
public function setBounceHandlerURL($bounceHandlerURL) {
@@ -195,6 +196,7 @@ class Email extends ViewableData {
} else {
user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE);
}
+ return $this;
}
public function Subject() {
@@ -223,26 +225,32 @@ class Email extends ViewableData {
public function setSubject($val) {
$this->subject = $val;
+ return $this;
}
public function setBody($val) {
$this->body = $val;
+ return $this;
}
public function setTo($val) {
$this->to = $val;
+ return $this;
}
public function setFrom($val) {
$this->from = $val;
+ return $this;
}
public function setCc($val) {
$this->cc = $val;
+ return $this;
}
public function setBcc($val) {
$this->bcc = $val;
+ return $this;
}
/**
@@ -251,6 +259,7 @@ class Email extends ViewableData {
*/
public function replyTo($email) {
$this->addCustomHeader('Reply-To', $email);
+ return $this;
}
/**
@@ -267,6 +276,7 @@ class Email extends ViewableData {
if(isset($this->customHeaders[$headerName])) $this->customHeaders[$headerName] .= ", " . $headerValue;
else $this->customHeaders[$headerName] = $headerValue;
}
+ return $this;
}
public function BaseURL() {
@@ -295,6 +305,7 @@ class Email extends ViewableData {
*/
public function setTemplate($template) {
$this->ss_template = $template;
+ return $this;
}
/**
@@ -340,6 +351,8 @@ class Email extends ViewableData {
$this->template_data = $this->customise($data);
}
$this->parseVariables_done = false;
+
+ return $this;
}
/**
@@ -375,6 +388,8 @@ class Email extends ViewableData {
$this->body = HTTP::absoluteURLs($fullBody);
}
Config::inst()->update('SSViewer', 'source_file_comments', $origState);
+
+ return $this;
}
/**
diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php
index 7082881be..c8202b394 100644
--- a/forms/ConfirmedPasswordField.php
+++ b/forms/ConfirmedPasswordField.php
@@ -251,8 +251,12 @@ class ConfirmedPasswordField extends FormField {
// If $data is a DataObject, don't use the value, since it's a hashed value
if ($data && $data instanceof DataObject) $value = '';
+ //store this for later
+ $oldValue = $this->value;
+
if(is_array($value)) {
- if($value['_Password'] || (!$value['_Password'] && !$this->canBeEmpty)) {
+ //only set the value if it's valid!
+ if($this->validate(RequiredFields::create())) {
$this->value = $value['_Password'];
}
@@ -266,11 +270,14 @@ class ConfirmedPasswordField extends FormField {
}
}
- $this->children->fieldByName($this->getName() . '[_Password]')
- ->setValue($this->value);
+ //looking up field by name is expensive, so lets check it needs to change
+ if ($oldValue != $this->value) {
+ $this->children->fieldByName($this->getName() . '[_Password]')
+ ->setValue($this->value);
- $this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
- ->setValue($this->value);
+ $this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
+ ->setValue($this->value);
+ }
return $this;
}
@@ -359,7 +366,9 @@ class ConfirmedPasswordField extends FormField {
}
$limitRegex = '/^.' . $limit . '$/';
if(!empty($value) && !preg_match($limitRegex,$value)) {
- $validator->validationError('Password', $errorMsg,
+ $validator->validationError(
+ $name,
+ $errorMsg,
"validation",
false
);
@@ -369,7 +378,7 @@ class ConfirmedPasswordField extends FormField {
if($this->requireStrongPassword) {
if(!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/',$value)) {
$validator->validationError(
- 'Password',
+ $name,
_t('Form.VALIDATIONSTRONGPASSWORD',
"Passwords must have at least one digit and one alphanumeric character"),
"validation",
diff --git a/forms/RequiredFields.php b/forms/RequiredFields.php
index 1e3adbd08..960a2bcde 100644
--- a/forms/RequiredFields.php
+++ b/forms/RequiredFields.php
@@ -1,10 +1,12 @@
required = array();
+
return $this;
}
@@ -50,7 +55,9 @@ class RequiredFields extends Validator {
* Debug helper
*/
public function debug() {
- if(!is_array($this->required)) return false;
+ if(!is_array($this->required)) {
+ return false;
+ }
$result = "
";
foreach( $this->required as $name ){
@@ -62,25 +69,40 @@ class RequiredFields extends Validator {
}
/**
- * Allows validation of fields via specification of a php function for validation which is executed after
- * the form is submitted
- */
+ * Allows validation of fields via specification of a php function for
+ * validation which is executed after the form is submitted.
+ *
+ * @param array $data
+ *
+ * @return boolean
+ */
public function php($data) {
$valid = true;
-
$fields = $this->form->Fields();
+
foreach($fields as $field) {
$valid = ($field->validate($this) && $valid);
}
+
if($this->required) {
foreach($this->required as $fieldName) {
- if(!$fieldName) continue;
+ if(!$fieldName) {
+ continue;
+ }
- $formField = $fields->dataFieldByName($fieldName);
+ if($fieldName instanceof FormField) {
+ $formField = $fieldName;
+ $fieldName = $fieldName->getName();
+ }
+ else {
+ $formField = $fields->dataFieldByName($fieldName);
+ }
$error = true;
+
// submitted data for file upload fields come back as an array
$value = isset($data[$fieldName]) ? $data[$fieldName] : null;
+
if(is_array($value)) {
if($formField instanceof FileField && isset($value['error']) && $value['error']) {
$error = true;
@@ -106,11 +128,13 @@ class RequiredFields extends Validator {
if($msg = $formField->getCustomValidationMessage()) {
$errorMessage = $msg;
}
+
$this->validationError(
$fieldName,
$errorMessage,
"required"
);
+
$valid = false;
}
}
@@ -120,40 +144,66 @@ class RequiredFields extends Validator {
}
/**
- * Add's a single required field to requiredfields stack
+ * Adds a single required field to required fields stack.
+ *
+ * @param string $field
+ *
+ * @return RequiredFields
*/
- public function addRequiredField( $field ) {
+ public function addRequiredField($field) {
$this->required[$field] = $field;
- return $this;
- }
- public function removeRequiredField($field) {
- unset($this->required[$field]);
return $this;
}
/**
- * allows you too add more required fields to this object after construction.
+ * Removes a required field
+ *
+ * @param string $field
+ *
+ * @return RequiredFields
*/
- public function appendRequiredFields($requiredFields){
- $this->required = $this->required + ArrayLib::valuekey($requiredFields->getRequired());
+ public function removeRequiredField($field) {
+ unset($this->required[$field]);
+
+ return $this;
+ }
+
+ /**
+ * Add {@link RequiredField} objects together
+ *
+ * @param RequiredFields
+ *
+ * @return RequiredFields
+ */
+ public function appendRequiredFields($requiredFields) {
+ $this->required = $this->required + ArrayLib::valuekey(
+ $requiredFields->getRequired()
+ );
+
return $this;
}
/**
* Returns true if the named field is "required".
- * Used by FormField to return a value for FormField::Required(), to do things like show *s on the form template.
+ *
+ * Used by {@link FormField} to return a value for FormField::Required(),
+ * to do things like show *s on the form template.
+ *
+ * @param string $fieldName
+ *
+ * @return boolean
*/
public function fieldIsRequired($fieldName) {
return isset($this->required[$fieldName]);
}
/**
- * getter function for append
+ * Return the required fields
+ *
+ * @return array
*/
- public function getRequired(){
+ public function getRequired() {
return array_values($this->required);
}
}
-
-
diff --git a/forms/gridfield/GridFieldConfig.php b/forms/gridfield/GridFieldConfig.php
index fcf6b55fd..5d0131126 100644
--- a/forms/gridfield/GridFieldConfig.php
+++ b/forms/gridfield/GridFieldConfig.php
@@ -236,7 +236,7 @@ class GridFieldConfig_RelationEditor extends GridFieldConfig {
$this->addComponent(new GridFieldButtonRow('before'));
$this->addComponent(new GridFieldAddNewButton('buttons-before-left'));
- $this->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-left'));
+ $this->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right'));
$this->addComponent(new GridFieldToolbarHeader());
$this->addComponent($sort = new GridFieldSortableHeader());
$this->addComponent($filter = new GridFieldFilterHeader());
diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php
index 688b8612a..781b368d0 100644
--- a/forms/gridfield/GridFieldDetailForm.php
+++ b/forms/gridfield/GridFieldDetailForm.php
@@ -349,7 +349,8 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
if($canDelete) {
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
->setUseButtonTag(true)
- ->addExtraClass('ss-ui-action-destructive action-delete'));
+ ->addExtraClass('ss-ui-action-destructive action-delete')
+ ->setAttribute('data-icon', 'delete'));
}
}else{ // adding new record
diff --git a/i18n/i18nTextCollector.php b/i18n/i18nTextCollector.php
index 8741bbd2c..30768f005 100644
--- a/i18n/i18nTextCollector.php
+++ b/i18n/i18nTextCollector.php
@@ -637,6 +637,13 @@ class i18nTextCollector_Parser extends SSTemplateParser {
private static $currentEntity = array();
+ public function __construct($string) {
+ $this->string = $string;
+ $this->pos = 0;
+ $this->depth = 0;
+ $this->regexps = array();
+ }
+
public function Translate__construct(&$res) {
self::$currentEntity = array(null,null,null); //start with empty array
}
diff --git a/javascript/UploadField.js b/javascript/UploadField.js
index 5a4b076a5..d66ce4c88 100644
--- a/javascript/UploadField.js
+++ b/javascript/UploadField.js
@@ -459,7 +459,8 @@
if(this.height() === 0) {
text = ss.i18n._t('UploadField.Editing', "Editing ...");
this.fitHeight();
- itemInfo.find('.toggle-details-icon').addClass('opened');
+ this.addClass('opened');
+ itemInfo.find('.toggle-details-icon').addClass('opened');
status.removeClass('ui-state-success-text').removeClass('ui-state-warning-text');
iframe.find('#Form_EditForm_action_doEdit').click(function(){
itemInfo.find('label .name').text(iframe.find('#Name input').val());
@@ -470,6 +471,7 @@
} else {
this.animate({height: 0}, 500);
+ this.removeClass('opened');
itemInfo.find('.toggle-details-icon').removeClass('opened');
$('div.ss-upload .ss-uploadfield-item-edit-all').removeClass('opened').find('.toggle-details-icon').removeClass('opened');
if(!this.hasClass('edited')){
@@ -493,9 +495,11 @@
});
$('div.ss-upload .ss-uploadfield-item-editform iframe').entwine({
onmatch: function() {
+ var form = this.closest('.ss-uploadfield-item-editform');
// TODO entwine event binding doesn't work for iframes
this.load(function() {
- $(this).parent().removeClass('loading');
+ $(this).parent().removeClass('loading');
+ if(form.hasClass('opened')) form.fitHeight();
});
this._super();
},
diff --git a/model/Versioned.php b/model/Versioned.php
index 2d30dae5b..9ff961f62 100644
--- a/model/Versioned.php
+++ b/model/Versioned.php
@@ -1289,21 +1289,7 @@ class Versioned extends DataExtension {
return $list;
}
-
- /**
- * @param Controller $controller
- */
- public function contentcontrollerInit($controller) {
- self::choose_site_stage();
- }
- /**
- * @param Controller $controller
- */
- public function modelascontrollerInit($controller) {
- self::choose_site_stage();
- }
-
/**
* @param array $labels
*/
diff --git a/scss/GridField.scss b/scss/GridField.scss
index 1244105df..e7710c021 100644
--- a/scss/GridField.scss
+++ b/scss/GridField.scss
@@ -118,14 +118,23 @@ $gf_grid_x: 16px;
margin-bottom: 6px;
}
.add-existing-autocompleter {
- span {
+ span {
+ float: left;
@include inline-block(top);
}
input.relation-search {
- width: 270px;
+ width: 270px; height: 32px;
margin-bottom: $gf_grid_y;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ button#action_gridfield_relationadd {
+ height: 32px;
+ margin-left: 0; // Webkit needs this
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left: none;
}
- width: 500px;
}
.grid-csv-button, .grid-print-button {
margin-bottom: 0;
diff --git a/security/Member.php b/security/Member.php
index 2bee1adb3..5ff27cd97 100644
--- a/security/Member.php
+++ b/security/Member.php
@@ -602,8 +602,21 @@ class Member extends DataObject implements TemplateGlobalProvider {
return $fields;
}
+ /**
+ * Returns the {@link RequiredFields} instance for the Member object. This
+ * Validator is used when saving a {@link CMSProfileController} or added to
+ * any form responsible for saving a users data.
+ *
+ * To customize the required fields, add a {@link DataExtension} to member
+ * calling the `updateValidator()` method.
+ *
+ * @return Member_Validator
+ */
public function getValidator() {
- return new Member_Validator();
+ $validator = Injector::inst()->create('Member_Validator');
+ $this->extend('updateValidator', $validator);
+
+ return $validator;
}
@@ -615,6 +628,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
*/
public static function currentUser() {
$id = Member::currentUserID();
+
if($id) {
return DataObject::get_one("Member", "\"Member\".\"ID\" = $id", true, 1);
}
@@ -1503,18 +1517,21 @@ class Member_GroupSet extends ManyManyList {
/**
* Class used as template to send an email saying that the password has been
- * changed
+ * changed.
+ *
* @package framework
* @subpackage security
*/
class Member_ChangePasswordEmail extends Email {
+
protected $from = ''; // setting a blank from address uses the site's default administrator email
protected $subject = '';
protected $ss_template = 'ChangePasswordEmail';
public function __construct() {
parent::__construct();
- $this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
+
+ $this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
}
}
@@ -1522,6 +1539,7 @@ class Member_ChangePasswordEmail extends Email {
/**
* Class used as template to send the forgot password email
+ *
* @package framework
* @subpackage security
*/
@@ -1532,18 +1550,29 @@ class Member_ForgotPasswordEmail extends Email {
public function __construct() {
parent::__construct();
- $this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
+
+ $this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
}
}
/**
* Member Validator
+ *
+ * Custom validation for the Member object can be achieved either through an
+ * {@link DataExtension} on the Member object or, by specifying a subclass of
+ * {@link Member_Validator} through the {@link Injector} API.
+ *
+ * {@see Member::getValidator()}
+ *
* @package framework
* @subpackage security
*/
class Member_Validator extends RequiredFields {
- protected $customRequired = array('FirstName', 'Email'); //, 'Password');
+ protected $customRequired = array(
+ 'FirstName',
+ 'Email'
+ );
/**
@@ -1551,15 +1580,16 @@ class Member_Validator extends RequiredFields {
*/
public function __construct() {
$required = func_get_args();
+
if(isset($required[0]) && is_array($required[0])) {
$required = $required[0];
}
+
$required = array_merge($required, $this->customRequired);
parent::__construct($required);
}
-
/**
* Check if the submitted member data is valid (server-side)
*
diff --git a/tests/behat/features/bootstrap/FeatureContext.php b/tests/behat/features/bootstrap/FeatureContext.php
index bf8568fa3..979adac1e 100644
--- a/tests/behat/features/bootstrap/FeatureContext.php
+++ b/tests/behat/features/bootstrap/FeatureContext.php
@@ -6,6 +6,7 @@ use SilverStripe\BehatExtension\Context\SilverStripeContext,
SilverStripe\BehatExtension\Context\BasicContext,
SilverStripe\BehatExtension\Context\LoginContext,
SilverStripe\BehatExtension\Context\FixtureContext,
+ SilverStripe\BehatExtension\Context\EmailContext,
SilverStripe\Framework\Test\Behaviour\CmsFormsContext,
SilverStripe\Framework\Test\Behaviour\CmsUiContext;
@@ -41,6 +42,7 @@ class FeatureContext extends SilverStripeContext
$this->useContext('LoginContext', new LoginContext($parameters));
$this->useContext('CmsFormsContext', new CmsFormsContext($parameters));
$this->useContext('CmsUiContext', new CmsUiContext($parameters));
+ $this->useContext('EmailContext', new EmailContext($parameters));
$fixtureContext = new FixtureContext($parameters);
$fixtureContext->setFixtureFactory($this->getFixtureFactory());
diff --git a/tests/behat/features/lostpassword.feature b/tests/behat/features/lostpassword.feature
new file mode 100644
index 000000000..43c8c57d0
--- /dev/null
+++ b/tests/behat/features/lostpassword.feature
@@ -0,0 +1,22 @@
+@todo
+Feature: Lost Password
+ As a site owner
+ I want to be able to reset my password
+ Using my email
+
+ Background:
+ Given a "member" "Admin" with "Email"="admin@test.com"
+
+ Scenario: I can request a password reset by email
+ Given I go to "Security/login"
+ When I follow "I've lost my password"
+ And I fill in "admin@test.com" for "Email"
+ And I press the "Send me the password reset link" button
+ Then I should see "Password reset link sent to 'admin@test.com'"
+ And there should be an email to "admin@test.com" titled "Your password reset link"
+ When I click on the "password reset link" link in the email to "admin@test.com"
+ Then I should see "Please enter a new password"
+ When I fill in "newpassword" for "New Password"
+ And I fill in "newpassword" for "Confirm New Password"
+ And I press the "Change Password" button
+ Then the password for "admin@test.com" should be "newpassword"
\ No newline at end of file
diff --git a/tests/control/CMSProfileControllerTest.php b/tests/control/CMSProfileControllerTest.php
index d5611c3f3..9545a2c9a 100644
--- a/tests/control/CMSProfileControllerTest.php
+++ b/tests/control/CMSProfileControllerTest.php
@@ -1,4 +1,9 @@
orig['File_searchable'] = File::has_extension('FulltextSearchable');
-
- // TODO This shouldn't need all arguments included
- File::remove_extension('FulltextSearchable(\'"Filename","Title","Content"\')');
+
+ FulltextSearchable::enable('File');
}
-
+
+ /**
+ * FulltextSearchable::enable() leaves behind remains that don't get cleaned up
+ * properly at the end of the test. This becomes apparent when a later test tries to
+ * ALTER TABLE File and add fulltext indexes with the InnoDB table type.
+ */
public function tearDown() {
- // TODO This shouldn't need all arguments included
- if($this->orig['File_searchable']) {
- File::add_extension('FulltextSearchable(\'"Filename","Title","Content"\')');
- }
-
parent::tearDown();
+
+ File::remove_extension('FulltextSearchable');
+ Config::inst()->update('File', 'create_table_options', array('MySQLDatabase' => 'ENGINE=InnoDB'));
}
-
+
public function testEnable() {
- FulltextSearchable::enable();
$this->assertTrue(File::has_extension('FulltextSearchable'));
}
-
+
public function testEnableWithCustomClasses() {
FulltextSearchable::enable(array('File'));
$this->assertTrue(File::has_extension('FulltextSearchable'));
- // TODO This shouldn't need all arguments included
- File::remove_extension('FulltextSearchable(\'"Filename","Title","Content"\')');
-
+ File::remove_extension('FulltextSearchable');
$this->assertFalse(File::has_extension('FulltextSearchable'));
}
-
+
}
diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php
index 990e8b02d..b138488ee 100644
--- a/tests/security/MemberTest.php
+++ b/tests/security/MemberTest.php
@@ -7,7 +7,7 @@ class MemberTest extends FunctionalTest {
protected static $fixture_file = 'MemberTest.yml';
protected $orig = array();
- protected $local = null;
+ protected $local = null;
protected $illegalExtensions = array(
'Member' => array(
@@ -42,10 +42,11 @@ class MemberTest extends FunctionalTest {
public function tearDown() {
Member::config()->unique_identifier_field = $this->orig['Member_unique_identifier_field'];
-
parent::tearDown();
}
+
+
/**
* @expectedException ValidationException
*/
@@ -720,45 +721,111 @@ class MemberTest extends FunctionalTest {
$member->isLockedOut(),
"Member has been locked out too early"
);
- }
+ }
- //fail login until max login attempts is reached
- $member->FailedLoginCount = 0;
- for ($i = 0; $i < $maxFailedLoginsAllowed; ++$i) {
- $member->registerFailedLogin();
- }
- //check to see if they've been locked out
- $this->assertTrue(
- $member->isLockedOut(),
- 'Member was not locked out when max logins met'
- );
+ public function testCustomMemberValidator() {
+ $member = $this->objFromFixture('Member', 'admin');
- //after they're locked out, need to check FailedLoginCount was reset to 0
- $this->assertEquals(
- $member->FailedLoginCount,
- 0,
- 'Failed login count was not reset after lockout'
- );
+ $form = new MemberTest_ValidatorForm();
+ $form->loadDataFrom($member);
- //test all done, unnest config
- Config::unnest();
+ $validator = new Member_Validator();
+ $validator->setForm($form);
+
+ $pass = $validator->php(array(
+ 'FirstName' => 'Borris',
+ 'Email' => 'borris@silverstripe.com'
+ ));
+
+ $fail = $validator->php(array(
+ 'Email' => 'borris@silverstripe.com',
+ 'Surname' => ''
+ ));
+
+ $this->assertTrue($pass, 'Validator requires on FirstName and Email');
+ $this->assertFalse($fail, 'Missing FirstName');
+
+ $ext = new MemberTest_ValidatorExtension();
+ $ext->updateValidator($validator);
+
+ $pass = $validator->php(array(
+ 'FirstName' => 'Borris',
+ 'Email' => 'borris@silverstripe.com'
+ ));
+
+ $fail = $validator->php(array(
+ 'Email' => 'borris@silverstripe.com'
+ ));
+
+ $this->assertFalse($pass, 'Missing surname');
+ $this->assertFalse($fail, 'Missing surname value');
+
+ $fail = $validator->php(array(
+ 'Email' => 'borris@silverstripe.com',
+ 'Surname' => 'Silverman'
+ ));
+
+ $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
}
}
+
+/**
+ * @package framework
+ * @subpackage tests
+ */
+class MemberTest_ValidatorForm extends Form implements TestOnly {
+
+ public function __construct() {
+ parent::__construct(Controller::curr(), __CLASS__, new FieldList(
+ new TextField('Email'),
+ new TextField('Surname'),
+ new TextField('ID'),
+ new TextField('FirstName')
+ ), new FieldList(
+ new FormAction('someAction')
+ ));
+ }
+}
+
+/**
+ * @package framework
+ * @subpackage tests
+ */
+class MemberTest_ValidatorExtension extends DataExtension implements TestOnly {
+
+ public function updateValidator(&$validator) {
+ $validator->addRequiredField('Surname');
+ $validator->removeRequiredField('FirstName');
+ }
+}
+
+/**
+ * @package framework
+ * @subpackage tests
+ */
class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return true;
}
-
}
+
+/**
+ * @package framework
+ * @subpackage tests
+ */
class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return false;
}
-
}
+
+/**
+ * @package framework
+ * @subpackage tests
+ */
class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
@@ -775,6 +842,10 @@ class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension imp
}
+/**
+ * @package framework
+ * @subpackage tests
+ */
class MemberTest_PasswordValidator extends PasswordValidator {
public function __construct() {
parent::__construct();
diff --git a/view/SSTemplateParser.php b/view/SSTemplateParser.php
index f516aa6b0..f08bd945b 100644
--- a/view/SSTemplateParser.php
+++ b/view/SSTemplateParser.php
@@ -65,14 +65,20 @@ class SSTemplateParseException extends Exception {
* Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
* N: eats white space including newlines (using in legacy _t support)
*/
-class SSTemplateParser extends Parser {
+class SSTemplateParser extends Parser implements TemplateParser {
/**
* @var bool - Set true by SSTemplateParser::compileString if the template should include comments intended
* for debugging (template source, included files, etc)
*/
protected $includeDebuggingComments = false;
-
+
+ /**
+ * Override the Parser constructor to change the requirement of setting a string
+ */
+ function __construct() {
+ }
+
/**
* Override the function that constructs the result arrays to also prepare a 'php' item in the array
*/
@@ -1757,7 +1763,7 @@ class SSTemplateParser extends Parser {
/* CacheBlockArgument:
!( "if " | "unless " )
(
- :DollarMarkedLookup |
+ :DollarMarkedLookup |
:QuotedString |
:Lookup
) */
@@ -4548,7 +4554,8 @@ class SSTemplateParser extends Parser {
// non-dynamically calculated
$text = preg_replace(
'/href\s*\=\s*\"\#/',
- 'href="\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ? strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
+ 'href="\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
+ ' strip_tags( $_SERVER[\'REQUEST_URI\'] ) : "") .
\'#',
$text
);
@@ -4563,29 +4570,28 @@ class SSTemplateParser extends Parser {
/**
* Compiles some passed template source code into the php code that will execute as per the template source.
*
- * @static
* @throws SSTemplateParseException
* @param $string The source of the template
* @param string $templateName The name of the template, normally the filename the template source was loaded from
* @param bool $includeDebuggingComments True is debugging comments should be included in the output
* @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
*/
- static function compileString($string, $templateName = "", $includeDebuggingComments=false) {
+ public function compileString($string, $templateName = "", $includeDebuggingComments=false) {
if (!trim($string)) {
$code = '';
}
else {
- // Construct a parser instance
- $parser = new SSTemplateParser($string);
- $parser->includeDebuggingComments = $includeDebuggingComments;
+ parent::__construct($string);
+
+ $this->includeDebuggingComments = $includeDebuggingComments;
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
// (and other encodings) properly
- if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $parser->pos = 3;
+ if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3;
// Match the source against the parser
- $result = $parser->match_TopTemplate();
- if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
+ $result = $this->match_TopTemplate();
+ if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this);
// Get the result
$code = $result['php'];
@@ -4593,7 +4599,7 @@ class SSTemplateParser extends Parser {
// Include top level debugging comments if desired
if($includeDebuggingComments && $templateName && stripos($code, "includeDebuggingComments($code, $templateName);
+ $code = $this->includeDebuggingComments($code, $templateName);
}
return $code;
@@ -4640,7 +4646,7 @@ class SSTemplateParser extends Parser {
* @param $template - A file path that contains template source code
* @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
*/
- static function compileFile($template) {
- return self::compileString(file_get_contents($template), $template);
+ public function compileFile($template) {
+ return $this->compileString(file_get_contents($template), $template);
}
}
diff --git a/view/SSTemplateParser.php.inc b/view/SSTemplateParser.php.inc
index e90860953..4a6c2e30f 100644
--- a/view/SSTemplateParser.php.inc
+++ b/view/SSTemplateParser.php.inc
@@ -86,14 +86,20 @@ class SSTemplateParseException extends Exception {
* Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
* N: eats white space including newlines (using in legacy _t support)
*/
-class SSTemplateParser extends Parser {
+class SSTemplateParser extends Parser implements TemplateParser {
/**
* @var bool - Set true by SSTemplateParser::compileString if the template should include comments intended
* for debugging (template source, included files, etc)
*/
protected $includeDebuggingComments = false;
-
+
+ /**
+ * Override the Parser constructor to change the requirement of setting a string
+ */
+ function __construct() {
+ }
+
/**
* Override the function that constructs the result arrays to also prepare a 'php' item in the array
*/
@@ -462,7 +468,7 @@ class SSTemplateParser extends Parser {
CacheBlockArgument:
!( "if " | "unless " )
(
- :DollarMarkedLookup |
+ :DollarMarkedLookup |
:QuotedString |
:Lookup
)
@@ -1018,29 +1024,28 @@ class SSTemplateParser extends Parser {
/**
* Compiles some passed template source code into the php code that will execute as per the template source.
*
- * @static
* @throws SSTemplateParseException
* @param $string The source of the template
* @param string $templateName The name of the template, normally the filename the template source was loaded from
* @param bool $includeDebuggingComments True is debugging comments should be included in the output
* @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
*/
- static function compileString($string, $templateName = "", $includeDebuggingComments=false) {
+ public function compileString($string, $templateName = "", $includeDebuggingComments=false) {
if (!trim($string)) {
$code = '';
}
else {
- // Construct a parser instance
- $parser = new SSTemplateParser($string);
- $parser->includeDebuggingComments = $includeDebuggingComments;
+ parent::__construct($string);
+
+ $this->includeDebuggingComments = $includeDebuggingComments;
// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
// (and other encodings) properly
- if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $parser->pos = 3;
+ if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3;
// Match the source against the parser
- $result = $parser->match_TopTemplate();
- if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
+ $result = $this->match_TopTemplate();
+ if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this);
// Get the result
$code = $result['php'];
@@ -1048,7 +1053,7 @@ class SSTemplateParser extends Parser {
// Include top level debugging comments if desired
if($includeDebuggingComments && $templateName && stripos($code, "includeDebuggingComments($code, $templateName);
+ $code = $this->includeDebuggingComments($code, $templateName);
}
return $code;
@@ -1095,7 +1100,7 @@ class SSTemplateParser extends Parser {
* @param $template - A file path that contains template source code
* @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
*/
- static function compileFile($template) {
- return self::compileString(file_get_contents($template), $template);
+ public function compileFile($template) {
+ return $this->compileString(file_get_contents($template), $template);
}
}
diff --git a/view/SSViewer.php b/view/SSViewer.php
index 9a21febfe..b94e86382 100644
--- a/view/SSViewer.php
+++ b/view/SSViewer.php
@@ -614,6 +614,11 @@ class SSViewer {
*/
protected $includeRequirements = true;
+ /**
+ * @var TemplateParser
+ */
+ protected $parser;
+
/**
* Create a template from a string instead of a .ss file
*
@@ -691,7 +696,9 @@ class SSViewer {
* array('MySpecificPage', 'MyPage', 'Page')
*
*/
- public function __construct($templateList) {
+ public function __construct($templateList, TemplateParser $parser = null) {
+ $this->setParser($parser ?: Injector::inst()->get('SSTemplateParser'));
+
// flush template manifest cache if requested
if (isset($_GET['flush']) && $_GET['flush'] == 'all') {
if(Director::isDev() || Director::is_cli() || Permission::check('ADMIN')) {
@@ -728,7 +735,25 @@ class SSViewer {
);
}
}
-
+
+ /**
+ * Set the template parser that will be used in template generation
+ * @param \TemplateParser $parser
+ */
+ public function setParser(TemplateParser $parser)
+ {
+ $this->parser = $parser;
+ }
+
+ /**
+ * Returns the parser that is set for template generation
+ * @return \TemplateParser
+ */
+ public function getParser()
+ {
+ return $this->parser;
+ }
+
/**
* Returns true if at least one of the listed templates exists.
*
@@ -970,7 +995,7 @@ class SSViewer {
if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited || isset($_GET['flush'])) {
$content = file_get_contents($template);
- $content = SSViewer::parseTemplateContent($content, $template);
+ $content = $this->parseTemplateContent($content, $template);
$fh = fopen($cacheFile,'w');
fwrite($fh, $content);
@@ -983,7 +1008,7 @@ class SSViewer {
// through $Content and $Layout placeholders.
foreach(array('Content', 'Layout') as $subtemplate) {
if(isset($this->chosenTemplates[$subtemplate])) {
- $subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]);
+ $subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate], $this->parser);
$subtemplateViewer->includeRequirements(false);
$subtemplateViewer->setPartialCacheStore($this->getPartialCacheStore());
@@ -1028,10 +1053,10 @@ class SSViewer {
return $v->process($data, $arguments, $scope);
}
- public static function parseTemplateContent($content, $template="") {
- return SSTemplateParser::compileString(
- $content,
- $template,
+ public function parseTemplateContent($content, $template="") {
+ return $this->parser->compileString(
+ $content,
+ $template,
Director::isDev() && Config::inst()->get('SSViewer', 'source_file_comments')
);
}
@@ -1079,7 +1104,8 @@ class SSViewer {
class SSViewer_FromString extends SSViewer {
protected $content;
- public function __construct($content) {
+ public function __construct($content, TemplateParser $parser = null) {
+ $this->setParser($parser ?: Injector::inst()->get('SSTemplateParser'));
$this->content = $content;
}
@@ -1091,7 +1117,7 @@ class SSViewer_FromString extends SSViewer {
$arguments = null;
}
- $template = SSViewer::parseTemplateContent($this->content, "string sha1=".sha1($this->content));
+ $template = $this->parseTemplateContent($this->content, "string sha1=".sha1($this->content));
$tmpFile = tempnam(TEMP_FOLDER,"");
$fh = fopen($tmpFile, 'w');
diff --git a/view/TemplateParser.php b/view/TemplateParser.php
new file mode 100644
index 000000000..a256ef007
--- /dev/null
+++ b/view/TemplateParser.php
@@ -0,0 +1,19 @@
+