From 68141b6ca0391ee11fec5fb10bae8381c7e20b71 Mon Sep 17 00:00:00 2001 From: ARNHOE Date: Sat, 7 Sep 2013 16:10:52 +0200 Subject: [PATCH 01/19] i18n documentation - added note for caching in multi language modules --- docs/en/topics/i18n.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/en/topics/i18n.md b/docs/en/topics/i18n.md index 027aaa023..d67deb531 100644 --- a/docs/en/topics/i18n.md +++ b/docs/en/topics/i18n.md @@ -205,6 +205,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 From 3851ef9f2cc3a9a0301b5367e8788dd839af65b7 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Fri, 11 Oct 2013 10:35:04 +1300 Subject: [PATCH 02/19] Style destructive actions destructively --- admin/css/ie7.css | 4 ++-- admin/css/ie8.css | 4 ++-- admin/css/screen.css | 7 ++++-- admin/scss/_forms.scss | 30 +++++++++++++++++++++++-- admin/scss/_style.scss | 5 +++++ admin/scss/themes/_default.scss | 3 ++- forms/gridfield/GridFieldDetailForm.php | 3 ++- 7 files changed, 46 insertions(+), 10 deletions(-) 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 8503e20ac..d74111b2a 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/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 From 27b139bc752bfa19b46b1ae9d2ecdce0661c25b4 Mon Sep 17 00:00:00 2001 From: Craig Lyons Date: Fri, 11 Oct 2013 10:18:00 -0400 Subject: [PATCH 03/19] Add backwards compatibility for has_extension --- core/Object.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/core/Object.php b/core/Object.php index 3c5480a47..b4251d2db 100755 --- a/core/Object.php +++ b/core/Object.php @@ -470,12 +470,21 @@ abstract class Object { } /** - * Return TRUE if a class has a specified extension - * - * @param string $requiredExtension the class name of the extension to check for. + * Return TRUE if a class has a specified extension. + * This supports backwards-compatible format (static Object::has_extension($requiredExtension)) and new format ($object->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'); From 8febaeafb97ecd4a63830aab1027d3607680cd32 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Tue, 15 Oct 2013 11:26:23 +1300 Subject: [PATCH 04/19] Update docblocks --- admin/code/AdminRootController.php | 4 ++++ admin/code/CMSBatchAction.php | 4 ++-- admin/code/CMSBatchActionHandler.php | 4 ++-- admin/code/CMSForm.php | 7 ++++++- admin/code/CMSPreviewable.php | 18 +++++++++++------- admin/code/CMSProfileController.php | 12 ++++++++++-- admin/code/GroupImportForm.php | 5 +++-- admin/code/LeftAndMainExtension.php | 1 + admin/code/MemberImportForm.php | 5 +++-- admin/code/SecurityAdmin.php | 11 ++++++++--- 10 files changed, 50 insertions(+), 21 deletions(-) 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 f364ad4ff..ef31e239e 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->setValidator(new Member_Validator()); $form->Actions()->push( FormAction::create('save',_t('CMSMain.SAVE', 'Save')) ->addExtraClass('ss-ui-button ss-ui-action-constructive') ->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'); 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', From 813d34b15ef43167022454a3a99ecd331833836e Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Wed, 16 Oct 2013 11:29:43 +1300 Subject: [PATCH 05/19] FIX: Use Injector API for managing Member_Validator instance. Updates the CMS profile page and SecurityAdmin to give developers a few ways to customise the required fields. Added extension hook updateValidator for getValidator for things like modules to inject required fields to go along with Injector for replacing the entire class for project specific use. --- admin/code/CMSProfileController.php | 14 ++- admin/code/SecurityAdmin.php | 11 ++- forms/RequiredFields.php | 108 +++++++++++++++------ security/Member.php | 42 ++++++-- tests/control/CMSProfileControllerTest.php | 10 ++ tests/security/MemberTest.php | 100 ++++++++++++++++++- 6 files changed, 244 insertions(+), 41 deletions(-) diff --git a/admin/code/CMSProfileController.php b/admin/code/CMSProfileController.php index 2d751dbd1..6b03f105e 100644 --- a/admin/code/CMSProfileController.php +++ b/admin/code/CMSProfileController.php @@ -31,9 +31,9 @@ class CMSProfileController extends LeftAndMain { if($form instanceof SS_HTTPResponse) { return $form; } + $form->Fields()->removeByName('LastVisited'); $form->Fields()->push(new HiddenField('ID', null, Member::currentUserID())); - $form->setValidator(new Member_Validator()); $form->Actions()->push( FormAction::create('save',_t('CMSMain.SAVE', 'Save')) ->addExtraClass('ss-ui-button ss-ui-action-constructive') @@ -44,7 +44,17 @@ class CMSProfileController extends LeftAndMain { $form->Actions()->removeByName('action_delete'); $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/SecurityAdmin.php b/admin/code/SecurityAdmin.php index 9a89badd1..2e71de0bc 100755 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -75,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/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/security/Member.php b/security/Member.php index a4f212a9f..8dac6ff40 100644 --- a/security/Member.php +++ b/security/Member.php @@ -611,8 +611,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; } @@ -624,6 +637,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); } @@ -1525,18 +1539,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'); } } @@ -1544,6 +1561,7 @@ class Member_ChangePasswordEmail extends Email { /** * Class used as template to send the forgot password email + * * @package framework * @subpackage security */ @@ -1554,18 +1572,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' + ); /** @@ -1573,15 +1602,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/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 @@ unique_identifier_field = $this->orig['Member_unique_identifier_field']; - parent::tearDown(); } + + /** * @expectedException ValidationException */ @@ -698,21 +699,110 @@ class MemberTest extends FunctionalTest { ); } + + public function testCustomMemberValidator() { + $member = $this->objFromFixture('Member', 'admin'); + + $form = new MemberTest_ValidatorForm(); + $form->loadDataFrom($member); + + $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) { @@ -729,6 +819,10 @@ class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension imp } +/** + * @package framework + * @subpackage tests + */ class MemberTest_PasswordValidator extends PasswordValidator { public function __construct() { parent::__construct(); From ac418ce99e3efe242615660a02f15baecfab5dfd Mon Sep 17 00:00:00 2001 From: Cam Spiers Date: Thu, 17 Oct 2013 21:28:12 +1300 Subject: [PATCH 06/19] Feature to allow that changing the SSTemplateParser through the Injector system The motivation for this was to allow module developers to change what parser is used to parse SilverStripe templates. This change enables people to compile their own version of the SilverStripe template parser and use it without modifying core files. --- i18n/i18nTextCollector.php | 7 ++++++ view/SSTemplateParser.php | 36 +++++++++++++++------------ view/SSTemplateParser.php.inc | 33 ++++++++++++++----------- view/SSViewer.php | 46 +++++++++++++++++++++++++++-------- view/TemplateParser.php | 19 +++++++++++++++ 5 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 view/TemplateParser.php diff --git a/i18n/i18nTextCollector.php b/i18n/i18nTextCollector.php index eb21c30f5..73b855598 100644 --- a/i18n/i18nTextCollector.php +++ b/i18n/i18nTextCollector.php @@ -634,6 +634,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/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 @@ + Date: Mon, 21 Oct 2013 00:49:31 +0200 Subject: [PATCH 07/19] Using Behat EmailContext by default --- tests/behat/features/bootstrap/FeatureContext.php | 2 ++ 1 file changed, 2 insertions(+) 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()); From 8ef14d2df4ea12a1e96270403ed1c2daad9838b0 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Mon, 21 Oct 2013 10:58:45 +0200 Subject: [PATCH 08/19] Fluent API for Email --- email/Email.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/email/Email.php b/email/Email.php index 6aa8c4811..99629559c 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; } /** @@ -536,6 +551,8 @@ class Email extends ViewableData { public static function setAdminEmail($newEmail) { Deprecation::notice('3.2', 'Use the "Email.admin_email" config setting instead'); Config::inst()->update('Email', 'admin_email', $newEmail); + + return $this; } /** From 2a6f1f1949956b4c91c5b7925707f29653dc1033 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 21 Oct 2013 20:31:40 +0100 Subject: [PATCH 09/19] FIX #2496 `ConfirmedPasswordField` mismatch passwords saved Fixes #2496 Also fixes another issue where 'Password' is hard coded as field name in `validate()` --- forms/ConfirmedPasswordField.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index 25498fe4b..35ee2224a 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -243,8 +243,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']; } @@ -258,11 +262,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; } @@ -351,7 +358,9 @@ class ConfirmedPasswordField extends FormField { } $limitRegex = '/^.' . $limit . '$/'; if(!empty($value) && !preg_match($limitRegex,$value)) { - $validator->validationError('Password', $errorMsg, + $validator->validationError( + $name, + $errorMsg, "validation", false ); @@ -361,7 +370,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", From 56d7c1fde2f6ad9669e7337fd5d717883a1d7525 Mon Sep 17 00:00:00 2001 From: mandrew Date: Tue, 22 Oct 2013 00:06:36 +0200 Subject: [PATCH 10/19] Behat tests for lost password --- tests/behat/features/lostpassword.feature | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/behat/features/lostpassword.feature diff --git a/tests/behat/features/lostpassword.feature b/tests/behat/features/lostpassword.feature new file mode 100644 index 000000000..14c773ad5 --- /dev/null +++ b/tests/behat/features/lostpassword.feature @@ -0,0 +1,21 @@ +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 From acaf0e40ccc5c0b0ba991c750ba15b905207a70f Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 22 Oct 2013 17:52:51 +1300 Subject: [PATCH 11/19] FulltextSearchableTest doesn't clean up after itself. The extension doesn't get unloaded correctly at the end of the test, resulting in tests afterwards sometimes failing because the table type is reset back to InnoDB. See silverstripe/silverstripe-cms ed8ee4e9b for a similar fix done in the cms module. --- tests/search/FulltextSearchableTest.php | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/tests/search/FulltextSearchableTest.php b/tests/search/FulltextSearchableTest.php index 7f8e3da06..1d7abb10e 100644 --- a/tests/search/FulltextSearchableTest.php +++ b/tests/search/FulltextSearchableTest.php @@ -8,35 +8,32 @@ class FulltextSearchableTest extends SapphireTest { public function setUp() { parent::setUp(); - - $this->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')); } - + } From 112e08e5f25b79f96b4afc84e9f6144b4aa355e3 Mon Sep 17 00:00:00 2001 From: Devlin Date: Tue, 22 Oct 2013 14:20:18 +0200 Subject: [PATCH 12/19] FIX Session::get_timeout --- control/Session.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'); } } From 39efaca54f7ad84c2a02c85b23316f037612507c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 22 Oct 2013 15:25:57 +0200 Subject: [PATCH 13/19] Disable "lost password" feature, breaks on "php -S" PHP's built-in webserver doesn't reliably expose SCRIPT_NAME, see https://github.com/silverstripe/silverstripe-framework/issues/2580. This breaks URL routing on /Security/passwordsent/admin%40test.com --- tests/behat/features/lostpassword.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/behat/features/lostpassword.feature b/tests/behat/features/lostpassword.feature index 14c773ad5..43c8c57d0 100644 --- a/tests/behat/features/lostpassword.feature +++ b/tests/behat/features/lostpassword.feature @@ -1,3 +1,4 @@ +@todo Feature: Lost Password As a site owner I want to be able to reset my password From e9fdfb05b258d29c7add514a8af597cd2f9cf338 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Tue, 22 Oct 2013 16:44:28 +0100 Subject: [PATCH 14/19] GridField 'Add existing' action styling improvements --- css/GridField.css | 8 ++++---- forms/gridfield/GridFieldConfig.php | 2 +- scss/GridField.scss | 15 ++++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) 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/forms/gridfield/GridFieldConfig.php b/forms/gridfield/GridFieldConfig.php index 78895d542..f9438284b 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/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; From 6f02c5bd98b13f5560ec4aae4f66a02e903129a4 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 22 Oct 2013 20:31:13 +0200 Subject: [PATCH 15/19] Email fluent API regression (fixes #2581) --- email/Email.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/email/Email.php b/email/Email.php index 99629559c..e3645434a 100644 --- a/email/Email.php +++ b/email/Email.php @@ -551,8 +551,6 @@ class Email extends ViewableData { public static function setAdminEmail($newEmail) { Deprecation::notice('3.2', 'Use the "Email.admin_email" config setting instead'); Config::inst()->update('Email', 'admin_email', $newEmail); - - return $this; } /** From 156bb87d57d2e6edda8360a96494bae545871b0f Mon Sep 17 00:00:00 2001 From: Andrew Short Date: Fri, 4 Oct 2013 00:20:32 +1000 Subject: [PATCH 16/19] FIX: Move stage choosing into a pre-request filter. This ensures that the correct stage is selected, even if the request does not come through the model as controller system. This fixes an issue where custom controllers would always be on the "Stage" stage. --- _config/requestprocessors.yml | 8 ++++++++ control/VersionedRequestFilter.php | 17 +++++++++++++++++ model/Versioned.php | 14 -------------- 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 _config/requestprocessors.yml create mode 100644 control/VersionedRequestFilter.php 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/control/VersionedRequestFilter.php b/control/VersionedRequestFilter.php new file mode 100644 index 000000000..c6158cc4f --- /dev/null +++ b/control/VersionedRequestFilter.php @@ -0,0 +1,17 @@ + Date: Wed, 23 Oct 2013 16:32:31 +1300 Subject: [PATCH 17/19] MINOR typo where display_errors wasn't checked properly --- dev/install/install.php5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/install/install.php5 b/dev/install/install.php5 index 438a29224..d393f6062 100755 --- a/dev/install/install.php5 +++ b/dev/install/install.php5 @@ -644,7 +644,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 { From 3422761a41830658fcee15efd7c8077c714c3c18 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 23 Oct 2013 11:16:47 +0200 Subject: [PATCH 18/19] Adjust UploadField edit form size Thanks @kinglozzer for getting this started. Fixes https://github.com/silverstripe/silverstripe-cms/issues/878 Replaces https://github.com/silverstripe/silverstripe-framework/pull/2221 --- javascript/UploadField.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/javascript/UploadField.js b/javascript/UploadField.js index a824deb8c..cecb3e61c 100644 --- a/javascript/UploadField.js +++ b/javascript/UploadField.js @@ -456,7 +456,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()); @@ -467,6 +468,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')){ @@ -490,9 +492,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(); }, From 55729dfc27cb0414cdbbdbc1d3fda19bb4ffcc18 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 23 Oct 2013 11:22:34 +0200 Subject: [PATCH 19/19] Removed deprecated log/email logic from Debug (fixes #2573) Was deprecated in 2009 with a682ab9c0e211167984b76790fb1458aae7ead7c, and removed in 2012 with 9eca2d676f9278ba528cb2ea6be4151703ed8fc2 --- dev/Debug.php | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) 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 {