API Updates to Form, ValidationResponse, ValidationException

API Implement form schema "errors" handling
This commit is contained in:
Damian Mooyman 2016-11-23 18:09:10 +13:00
parent 6650561dac
commit 6e589aac75
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
64 changed files with 7127 additions and 6816 deletions

View File

@ -1,6 +1,6 @@
webpackJsonp([5],[function(e,t,n){"use strict" webpackJsonp([5],[function(e,t,n){"use strict"
n(2),n(3),n(6),n(19),n(25),n(27),n(29),n(31),n(34),n(105),n(112),n(116),n(126),n(127),n(128),n(129),n(130),n(131),n(133),n(136),n(138),n(141),n(144),n(146),n(148),n(150),n(151),n(160),n(161),n(163),n(164), n(2),n(3),n(6),n(19),n(25),n(27),n(29),n(31),n(34),n(105),n(113),n(117),n(127),n(128),n(129),n(130),n(131),n(132),n(134),n(137),n(139),n(142),n(145),n(147),n(149),n(151),n(152),n(161),n(162),n(164),n(165),
n(165),n(166),n(167),n(168),n(169),n(170),n(171),n(172),n(173),n(174),n(175),n(178),n(180),n(181),n(182),n(183),n(187),n(188),n(189),n(190),n(191),n(188),n(183),n(194),n(195),n(197),n(198)},,function(e,t){ n(166),n(167),n(168),n(169),n(170),n(171),n(172),n(173),n(174),n(175),n(176),n(179),n(181),n(182),n(183),n(184),n(188),n(189),n(190),n(191),n(192),n(189),n(184),n(195),n(196),n(198),n(199)},,function(e,t){
"use strict" "use strict"
function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
@ -430,10 +430,10 @@ return Object.entries(e).reduce(function(e,n){var o=u(n,1),a=o[0],s=(0,m.findFie
if(d)return e if(d)return e
var h=p.map(function(e,t){return f["default"].createElement("span",{key:t,className:"form__validation-message"},e)}) var h=p.map(function(e,t){return f["default"].createElement("span",{key:t,className:"form__validation-message"},e)})
return l({},e,r({},a,{type:"error",value:{react:h}}))},{})}},{key:"handleAction",value:function i(e){"function"==typeof this.props.handleAction&&this.props.handleAction(e,this.props.values),e.isPropagationStopped()||this.setState({ return l({},e,r({},a,{type:"error",value:{react:h}}))},{})}},{key:"handleAction",value:function i(e){"function"==typeof this.props.handleAction&&this.props.handleAction(e,this.props.values),e.isPropagationStopped()||this.setState({
submittingAction:e.currentTarget.name})}},{key:"handleSubmit",value:function d(e){var t=this,n=this.state.submittingAction?this.state.submittingAction:this.props.schema.schema.actions[0].name,i=l({},e,r({},n,1)),o={ submittingAction:e.currentTarget.name})}},{key:"handleSubmit",value:function d(e){var t=this,n=this.state.submittingAction?this.state.submittingAction:this.props.schema.schema.actions[0].name,i=l({},e,r({},n,1)),o=this.props.responseRequestedSchema.join(),a={
"X-Formschema-Request":"state,schema","X-Requested-With":"XMLHttpRequest"},a=function s(e){return t.submitApi(e||i,o).then(function(e){return t.setState({submittingAction:null}),e})["catch"](function(e){ "X-Formschema-Request":o,"X-Requested-With":"XMLHttpRequest"},s=function u(e){return t.submitApi(e||i,a).then(function(e){return t.setState({submittingAction:null}),e})["catch"](function(e){throw t.setState({
throw t.setState({submittingAction:null}),e})} submittingAction:null}),e})}
return"function"==typeof this.props.handleSubmit?this.props.handleSubmit(i,n,a):a()}},{key:"buildComponent",value:function p(e){var t=e,n=null!==t.schemaComponent?E["default"].getComponentByName(t.schemaComponent):E["default"].getComponentByDataType(t.type) return"function"==typeof this.props.handleSubmit?this.props.handleSubmit(i,n,s):s()}},{key:"buildComponent",value:function p(e){var t=e,n=null!==t.schemaComponent?E["default"].getComponentByName(t.schemaComponent):E["default"].getComponentByDataType(t.type)
if(null===n)return null if(null===n)return null
@ -461,9 +461,11 @@ persistentSubmitErrors:p,validate:this.validateForm}
return f["default"].createElement(n,v)}}]),t}(y["default"]),O=d.PropTypes.shape({id:d.PropTypes.string,schema:d.PropTypes.shape({attributes:d.PropTypes.shape({"class":d.PropTypes.string,enctype:d.PropTypes.string return f["default"].createElement(n,v)}}]),t}(y["default"]),O=d.PropTypes.shape({id:d.PropTypes.string,schema:d.PropTypes.shape({attributes:d.PropTypes.shape({"class":d.PropTypes.string,enctype:d.PropTypes.string
}),fields:d.PropTypes.array.isRequired}),state:d.PropTypes.shape({fields:d.PropTypes.array}),loading:d.PropTypes["boolean"],stateOverride:d.PropTypes.shape({fields:d.PropTypes.array})}),S={createFn:d.PropTypes.func, }),fields:d.PropTypes.array.isRequired}),state:d.PropTypes.shape({fields:d.PropTypes.array}),loading:d.PropTypes["boolean"],stateOverride:d.PropTypes.shape({fields:d.PropTypes.array})}),S={createFn:d.PropTypes.func,
handleSubmit:d.PropTypes.func,handleAction:d.PropTypes.func,asyncValidate:d.PropTypes.func,onSubmitFail:d.PropTypes.func,onSubmitSuccess:d.PropTypes.func,shouldAsyncValidate:d.PropTypes.func,touchOnBlur:d.PropTypes.bool, handleSubmit:d.PropTypes.func,handleAction:d.PropTypes.func,asyncValidate:d.PropTypes.func,onSubmitFail:d.PropTypes.func,onSubmitSuccess:d.PropTypes.func,shouldAsyncValidate:d.PropTypes.func,touchOnBlur:d.PropTypes.bool,
touchOnChange:d.PropTypes.bool,persistentSubmitErrors:d.PropTypes.bool,validate:d.PropTypes.func,values:d.PropTypes.object,submitting:d.PropTypes.bool,baseFormComponent:d.PropTypes.func.isRequired,baseFieldComponent:d.PropTypes.func.isRequired touchOnChange:d.PropTypes.bool,persistentSubmitErrors:d.PropTypes.bool,validate:d.PropTypes.func,values:d.PropTypes.object,submitting:d.PropTypes.bool,baseFormComponent:d.PropTypes.func.isRequired,baseFieldComponent:d.PropTypes.func.isRequired,
} responseRequestedSchema:d.PropTypes.arrayOf(d.PropTypes.oneOf(["schema","state","errors","auto"]))}
P.propTypes=l({},S,{form:d.PropTypes.string.isRequired,schema:O.isRequired}),t.basePropTypes=S,t.schemaPropType=O,t["default"]=P},function(e,t){"use strict" P.propTypes=l({},S,{form:d.PropTypes.string.isRequired,schema:O.isRequired}),P.defaultProps={responseRequestedSchema:["auto"]},t.basePropTypes=S,t.schemaPropType=O,t["default"]=P},function(e,t){"use strict"
function n(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=null function n(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=null
if(!e)return n if(!e)return n
n=e.find(function(e){return e.name===t}) n=e.find(function(e){return e.name===t})
@ -542,39 +544,42 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){var n=e.schemas[t.schemaUrl],i=e.form&&e.form[t.schemaUrl],r=i&&i.submitting,o=i&&i.values,a=n&&n.stateOverride,s=n&&n.metadata&&n.metadata.loading e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){var n=e.schemas[t.schemaUrl],i=e.form&&e.form[t.schemaUrl],r=i&&i.submitting,o=i&&i.values,a=n&&n.stateOverride,s=n&&n.metadata&&n.metadata.loading
return{schema:n,submitting:r,values:o,stateOverrides:a,loading:s}}function u(e){return{schemaActions:(0,m.bindActionCreators)(_,e)}}Object.defineProperty(t,"__esModule",{value:!0}) return{schema:n,submitting:r,values:o,stateOverrides:a,loading:s}}function u(e){return{schemaActions:(0,m.bindActionCreators)(C,e)}}Object.defineProperty(t,"__esModule",{value:!0})
var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},d=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},d=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),f=n(5),p=r(f),h=n(107),m=n(108),g=n(8),v=r(g),y=n(109),b=n(110),_=i(b),w=n(17),C=r(w),T=n(26),E=r(T),P=n(111),O=r(P),S=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),f=n(5),p=r(f),h=n(107),m=n(108),g=n(8),v=r(g),y=n(109),b=r(y),_=n(110),w=n(111),C=i(w),T=n(17),E=r(T),P=n(26),O=r(P),S=n(112),k=r(S),j=function(e){
function t(e){o(this,t) function t(e){o(this,t)
var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handleSubmit=n.handleSubmit.bind(n),n.clearSchema=n.clearSchema.bind(n),n}return s(t,e),d(t,[{key:"componentDidMount",value:function n(){this.fetch()}},{key:"componentDidUpdate",value:function i(e){ return n.handleSubmit=n.handleSubmit.bind(n),n.clearSchema=n.clearSchema.bind(n),n.reduceSchemaErrors=n.reduceSchemaErrors.bind(n),n}return s(t,e),d(t,[{key:"componentDidMount",value:function n(){this.fetch()
this.props.schemaUrl!==e.schemaUrl&&(this.clearSchema(e.schemaUrl),this.fetch())}},{key:"componentWillUnmount",value:function r(){this.clearSchema(this.props.schemaUrl)}},{key:"getMessages",value:function l(e){
var t={} }},{key:"componentDidUpdate",value:function i(e){this.props.schemaUrl!==e.schemaUrl&&(this.clearSchema(e.schemaUrl),this.fetch())}},{key:"componentWillUnmount",value:function r(){this.clearSchema(this.props.schemaUrl)
return e&&e.fields&&e.fields.forEach(function(e){e.message&&(t[e.name]=e.message)}),t}},{key:"clearSchema",value:function u(e){e&&((0,y.destroy)(e),this.props.schemaActions.setSchema(e,null))}},{key:"handleSubmit",
}},{key:"getMessages",value:function l(e){var t={}
return e&&e.fields&&e.fields.forEach(function(e){e.message&&(t[e.name]=e.message)}),t}},{key:"clearSchema",value:function u(e){e&&((0,_.destroy)(e),this.props.schemaActions.setSchema(e,null))}},{key:"handleSubmit",
value:function f(e,t,n){var i=this,r=null value:function f(e,t,n){var i=this,r=null
if(r="function"==typeof this.props.handleSubmit?this.props.handleSubmit(e,t,n):n(),!r)throw new Error("Promise was not returned for submitting") if(r="function"==typeof this.props.handleSubmit?this.props.handleSubmit(e,t,n):n(),!r)throw new Error("Promise was not returned for submitting")
return r.then(function(e){return e&&i.props.schemaActions.setSchema(i.props.schemaUrl,e),e}).then(function(e){if(!e||!e.state)return e return r.then(function(e){var t=e
return t&&(t=i.reduceSchemaErrors(t),i.props.schemaActions.setSchema(i.props.schemaUrl,t)),t}).then(function(e){if(!e||!e.state)return e
var t=i.getMessages(e.state) var t=i.getMessages(e.state)
if(Object.keys(t).length)throw new y.SubmissionError(t) if(Object.keys(t).length)throw new _.SubmissionError(t)
return e})}},{key:"overrideStateData",value:function h(e){if(!this.props.stateOverrides||!e)return e return e})}},{key:"reduceSchemaErrors",value:function h(e){if(!e.errors)return e
var t=c({},e)
return t.state||(t=c({},t,{state:this.props.schema.state})),t=c({},t,{state:c({},t.state,{fields:t.state.fields.map(function(t){return c({},t,{message:e.errors.find(function(e){return e.field===t.name})
})}),messages:e.errors.filter(function(e){return!e.field})})}),delete t.errors,(0,b["default"])(t)}},{key:"overrideStateData",value:function m(e){if(!this.props.stateOverrides||!e)return e
var t=this.props.stateOverrides.fields,n=e.fields var t=this.props.stateOverrides.fields,n=e.fields
return t&&n&&(n=n.map(function(e){var n=t.find(function(t){return t.name===e.name}) return t&&n&&(n=n.map(function(e){var n=t.find(function(t){return t.name===e.name})
return n?C["default"].recursive(!0,e,n):e})),c({},e,this.props.stateOverrides,{fields:n})}},{key:"overrideStateData",value:function m(e){if(!this.props.stateOverrides||!e)return e return n?E["default"].recursive(!0,e,n):e})),c({},e,this.props.stateOverrides,{fields:n})}},{key:"fetch",value:function g(){var e=this,t=arguments.length<=0||void 0===arguments[0]||arguments[0],n=arguments.length<=1||void 0===arguments[1]||arguments[1],i=[]
var t=this.props.stateOverrides.fields,n=e.fields
return t&&n&&(n=n.map(function(e){var n=t.find(function(t){return t.name===e.name})
return n?C["default"].recursive(!0,e,n):e})),c({},e,this.props.stateOverrides,{fields:n})}},{key:"fetch",value:function g(){var e=this,t=arguments.length<=0||void 0===arguments[0]||arguments[0],n=arguments.length<=1||void 0===arguments[1]||arguments[1],i=[]
return t&&i.push("schema"),n&&i.push("state"),this.props.loading?Promise.resolve({}):(this.props.schemaActions.setSchemaLoading(this.props.schemaUrl,!0),(0,v["default"])(this.props.schemaUrl,{headers:{ return t&&i.push("schema"),n&&i.push("state"),this.props.loading?Promise.resolve({}):(this.props.schemaActions.setSchemaLoading(this.props.schemaUrl,!0),(0,v["default"])(this.props.schemaUrl,{headers:{
"X-FormSchema-Request":i.join()},credentials:"same-origin"}).then(function(e){return e.json()}).then(function(t){if(e.props.schemaActions.setSchemaLoading(e.props.schemaUrl,!1),"undefined"!=typeof t.id){ "X-FormSchema-Request":i.join()},credentials:"same-origin"}).then(function(e){return e.json()}).then(function(t){if(e.props.schemaActions.setSchemaLoading(e.props.schemaUrl,!1),"undefined"!=typeof t.id){
var n=c({},t,{state:e.overrideStateData(t.state)}) var n=c({},t,{state:e.overrideStateData(t.state)})
return e.props.schemaActions.setSchema(e.props.schemaUrl,n),n}return t}))}},{key:"render",value:function b(){if(!this.props.schema||!this.props.schema.schema||this.props.loading)return null return e.props.schemaActions.setSchema(e.props.schemaUrl,n),n}return t}))}},{key:"render",value:function y(){if(!this.props.schema||!this.props.schema.schema||this.props.loading)return null
var e=c({},this.props,{form:this.props.schemaUrl,onSubmitSuccess:this.props.onSubmitSuccess,handleSubmit:this.handleSubmit}) var e=c({},this.props,{form:this.props.schemaUrl,onSubmitSuccess:this.props.onSubmitSuccess,handleSubmit:this.handleSubmit})
return p["default"].createElement(O["default"],e)}}]),t}(f.Component) return p["default"].createElement(k["default"],e)}}]),t}(f.Component)
S.propTypes=c({},P.basePropTypes,{schemaActions:f.PropTypes.object.isRequired,schemaUrl:f.PropTypes.string.isRequired,schema:P.schemaPropType,form:f.PropTypes.string,submitting:f.PropTypes.bool}),S.defaultProps={ j.propTypes=c({},S.basePropTypes,{schemaActions:f.PropTypes.object.isRequired,schemaUrl:f.PropTypes.string.isRequired,schema:S.schemaPropType,form:f.PropTypes.string,submitting:f.PropTypes.bool}),j.defaultProps={
baseFormComponent:(0,y.reduxForm)()(E["default"]),baseFieldComponent:y.Field},t["default"]=(0,h.connect)(l,u)(S)},,,function(e,t){e.exports=ReduxForm},function(e,t){e.exports=SchemaActions},function(e,t){ baseFormComponent:(0,_.reduxForm)()(O["default"]),baseFieldComponent:_.Field},t["default"]=(0,h.connect)(l,u)(j)},,,function(e,t){e.exports=DeepFreezeStrict},function(e,t){e.exports=ReduxForm},function(e,t){
e.exports=FormBuilder},function(e,t,n){(function(t){e.exports=t.FormBuilderModal=n(113)}).call(t,function(){return this}())},function(e,t,n){"use strict" e.exports=SchemaActions},function(e,t){e.exports=FormBuilder},function(e,t,n){(function(t){e.exports=t.FormBuilderModal=n(114)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -582,7 +587,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}) value:!0})
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(114),d=i(c),f=n(22),p=n(21),h=i(p),m=n(115),g=i(m),v=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(115),d=i(c),f=n(22),p=n(21),h=i(p),m=n(116),g=i(m),v=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handleSubmit=n.handleSubmit.bind(n),n.handleHide=n.handleHide.bind(n),n.clearResponse=n.clearResponse.bind(n),n}return a(t,e),s(t,[{key:"getForm",value:function n(){return this.props.schemaUrl?u["default"].createElement(g["default"],{ return n.handleSubmit=n.handleSubmit.bind(n),n.handleHide=n.handleHide.bind(n),n.clearResponse=n.clearResponse.bind(n),n}return a(t,e),s(t,[{key:"getForm",value:function n(){return this.props.schemaUrl?u["default"].createElement(g["default"],{
@ -603,7 +608,7 @@ className:this.props.bodyClassName},t,e,this.props.children))}}]),t}(h["default"
v.propTypes={show:u["default"].PropTypes.bool,title:u["default"].PropTypes.oneOfType([u["default"].PropTypes.string,u["default"].PropTypes.bool]),className:u["default"].PropTypes.string,bodyClassName:u["default"].PropTypes.string, v.propTypes={show:u["default"].PropTypes.bool,title:u["default"].PropTypes.oneOfType([u["default"].PropTypes.string,u["default"].PropTypes.bool]),className:u["default"].PropTypes.string,bodyClassName:u["default"].PropTypes.string,
handleHide:u["default"].PropTypes.func,schemaUrl:u["default"].PropTypes.string,handleSubmit:u["default"].PropTypes.func,handleAction:u["default"].PropTypes.func,responseClassGood:u["default"].PropTypes.string, handleHide:u["default"].PropTypes.func,schemaUrl:u["default"].PropTypes.string,handleSubmit:u["default"].PropTypes.func,handleAction:u["default"].PropTypes.func,responseClassGood:u["default"].PropTypes.string,
responseClassBad:u["default"].PropTypes.string},v.defaultProps={show:!1,title:null},t["default"]=v},function(e,t){e.exports=i18n},function(e,t){e.exports=FormBuilderLoader},function(e,t,n){(function(t){ responseClassBad:u["default"].PropTypes.string},v.defaultProps={show:!1,title:null},t["default"]=v},function(e,t){e.exports=i18n},function(e,t){e.exports=FormBuilderLoader},function(e,t,n){(function(t){
e.exports=t.GridField=n(117)}).call(t,function(){return this}())},function(e,t,n){"use strict" e.exports=t.GridField=n(118)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){if(e&&e.__esModule)return e function i(e){if(e&&e.__esModule)return e
var t={} var t={}
if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]) if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])
@ -623,7 +628,7 @@ var i=Object.getOwnPropertyDescriptor(e,t)
if(void 0===i){var r=Object.getPrototypeOf(e) if(void 0===i){var r=Object.getPrototypeOf(e)
return null===r?void 0:M(r,t,n)}if("value"in i)return i.value return null===r?void 0:M(r,t,n)}if("value"in i)return i.value
var o=i.get var o=i.get
if(void 0!==o)return o.call(n)},f=n(5),p=r(f),h=n(108),m=n(107),g=n(21),v=r(g),y=n(118),b=r(y),_=n(119),w=r(_),C=n(121),T=r(C),E=n(120),P=r(E),O=n(122),S=r(O),k=n(123),j=r(k),x=n(28),R=r(x),I=n(124),A=i(I),D={},F=function(e){ if(void 0!==o)return o.call(n)},f=n(5),p=r(f),h=n(108),m=n(107),g=n(21),v=r(g),y=n(119),b=r(y),_=n(120),w=r(_),C=n(122),T=r(C),E=n(121),P=r(E),O=n(123),S=r(O),k=n(124),j=r(k),x=n(28),R=r(x),I=n(125),A=i(I),D={},F=function(e){
function t(e){o(this,t) function t(e){o(this,t)
var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.deleteRecord=n.deleteRecord.bind(n),n.editRecord=n.editRecord.bind(n),n}return s(t,e),c(t,[{key:"componentDidMount",value:function n(){d(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"componentDidMount",this).call(this) return n.deleteRecord=n.deleteRecord.bind(n),n.editRecord=n.editRecord.bind(n),n}return s(t,e),c(t,[{key:"componentDidMount",value:function n(){d(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"componentDidMount",this).call(this)
@ -672,7 +677,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}) value:!0})
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(120),p=i(f),h=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(121),p=i(f),h=function(e){
function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"render",value:function n(){return u["default"].createElement(p["default"],null,this.props.children) function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"render",value:function n(){return u["default"].createElement(p["default"],null,this.props.children)
}}]),t}(d["default"]) }}]),t}(d["default"])
@ -745,13 +750,13 @@ type:u["default"].FETCH_RECORD_FAILURE,payload:{error:n,recordType:e}}),n})}}fun
return function(n){return n({type:u["default"].DELETE_RECORD_REQUEST,payload:a}),d["default"][s].apply(d["default"],l).then(function(){n({type:u["default"].DELETE_RECORD_SUCCESS,payload:{recordType:e,id:t return function(n){return n({type:u["default"].DELETE_RECORD_REQUEST,payload:a}),d["default"][s].apply(d["default"],l).then(function(){n({type:u["default"].DELETE_RECORD_SUCCESS,payload:{recordType:e,id:t
}})})["catch"](function(i){throw n({type:u["default"].DELETE_RECORD_FAILURE,payload:{error:i,recordType:e,id:t}}),i})}}Object.defineProperty(t,"__esModule",{value:!0}),t.fetchRecords=o,t.fetchRecord=a, }})})["catch"](function(i){throw n({type:u["default"].DELETE_RECORD_FAILURE,payload:{error:i,recordType:e,id:t}}),i})}}Object.defineProperty(t,"__esModule",{value:!0}),t.fetchRecords=o,t.fetchRecord=a,
t.deleteRecord=s t.deleteRecord=s
var l=n(125),u=i(l),c=n(7),d=i(c)},function(e,t){"use strict" var l=n(126),u=i(l),c=n(7),d=i(c)},function(e,t){"use strict"
Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={CREATE_RECORD:"CREATE_RECORD",UPDATE_RECORD:"UPDATE_RECORD",DELETE_RECORD:"DELETE_RECORD",FETCH_RECORDS_REQUEST:"FETCH_RECORDS_REQUEST",FETCH_RECORDS_FAILURE:"FETCH_RECORDS_FAILURE", Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={CREATE_RECORD:"CREATE_RECORD",UPDATE_RECORD:"UPDATE_RECORD",DELETE_RECORD:"DELETE_RECORD",FETCH_RECORDS_REQUEST:"FETCH_RECORDS_REQUEST",FETCH_RECORDS_FAILURE:"FETCH_RECORDS_FAILURE",
FETCH_RECORDS_SUCCESS:"FETCH_RECORDS_SUCCESS",FETCH_RECORD_REQUEST:"FETCH_RECORD_REQUEST",FETCH_RECORD_FAILURE:"FETCH_RECORD_FAILURE",FETCH_RECORD_SUCCESS:"FETCH_RECORD_SUCCESS",DELETE_RECORD_REQUEST:"DELETE_RECORD_REQUEST", FETCH_RECORDS_SUCCESS:"FETCH_RECORDS_SUCCESS",FETCH_RECORD_REQUEST:"FETCH_RECORD_REQUEST",FETCH_RECORD_FAILURE:"FETCH_RECORD_FAILURE",FETCH_RECORD_SUCCESS:"FETCH_RECORD_SUCCESS",DELETE_RECORD_REQUEST:"DELETE_RECORD_REQUEST",
DELETE_RECORD_FAILURE:"DELETE_RECORD_FAILURE",DELETE_RECORD_SUCCESS:"DELETE_RECORD_SUCCESS"}},function(e,t,n){(function(t){e.exports=t.GridFieldCell=n(122)}).call(t,function(){return this}())},function(e,t,n){ DELETE_RECORD_FAILURE:"DELETE_RECORD_FAILURE",DELETE_RECORD_SUCCESS:"DELETE_RECORD_SUCCESS"}},function(e,t,n){(function(t){e.exports=t.GridFieldCell=n(123)}).call(t,function(){return this}())},function(e,t,n){
(function(t){e.exports=t.GridFieldHeader=n(119)}).call(t,function(){return this}())},function(e,t,n){(function(t){e.exports=t.GridFieldHeaderCell=n(121)}).call(t,function(){return this}())},function(e,t,n){ (function(t){e.exports=t.GridFieldHeader=n(120)}).call(t,function(){return this}())},function(e,t,n){(function(t){e.exports=t.GridFieldHeaderCell=n(122)}).call(t,function(){return this}())},function(e,t,n){
(function(t){e.exports=t.GridFieldRow=n(120)}).call(t,function(){return this}())},function(e,t,n){(function(t){e.exports=t.GridFieldTable=n(118)}).call(t,function(){return this}())},function(e,t,n){(function(t){ (function(t){e.exports=t.GridFieldRow=n(121)}).call(t,function(){return this}())},function(e,t,n){(function(t){e.exports=t.GridFieldTable=n(119)}).call(t,function(){return this}())},function(e,t,n){(function(t){
e.exports=t.HiddenField=n(132)}).call(t,function(){return this}())},function(e,t,n){"use strict" e.exports=t.HiddenField=n(133)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -765,7 +770,7 @@ className:this.props.className+" "+this.props.extraClass,id:this.props.id,name:t
}}]),t}(d["default"]) }}]),t}(d["default"])
p.propTypes={id:u["default"].PropTypes.string,extraClass:u["default"].PropTypes.string,name:u["default"].PropTypes.string.isRequired,value:u["default"].PropTypes.any},p.defaultProps={className:"",extraClass:"", p.propTypes={id:u["default"].PropTypes.string,extraClass:u["default"].PropTypes.string,name:u["default"].PropTypes.string.isRequired,value:u["default"].PropTypes.any},p.defaultProps={className:"",extraClass:"",
value:""},t["default"]=p},function(e,t,n){(function(t){e.exports=t.TextField=n(134)}).call(t,function(){return this}())},function(e,t,n){"use strict" value:""},t["default"]=p},function(e,t,n){(function(t){e.exports=t.TextField=n(135)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -774,7 +779,7 @@ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,wri
value:!0}),t.TextField=void 0 value:!0}),t.TextField=void 0
var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(135),h=i(p),m=n(22),g=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(136),h=i(p),m=n(22),g=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handleChange=n.handleChange.bind(n),n}return a(t,e),l(t,[{key:"render",value:function n(){var e=null return n.handleChange=n.handleChange.bind(n),n}return a(t,e),l(t,[{key:"render",value:function n(){var e=null
@ -786,7 +791,7 @@ return this.props.readOnly||(s(e,{placeholder:this.props.placeholder,onChange:th
id:this.props.id,value:e.target.value})}}]),t}(f["default"]) id:this.props.id,value:e.target.value})}}]),t}(f["default"])
g.propTypes={extraClass:c["default"].PropTypes.string,id:c["default"].PropTypes.string,name:c["default"].PropTypes.string.isRequired,onChange:c["default"].PropTypes.func,value:c["default"].PropTypes.oneOfType([c["default"].PropTypes.string,c["default"].PropTypes.number]), g.propTypes={extraClass:c["default"].PropTypes.string,id:c["default"].PropTypes.string,name:c["default"].PropTypes.string.isRequired,onChange:c["default"].PropTypes.func,value:c["default"].PropTypes.oneOfType([c["default"].PropTypes.string,c["default"].PropTypes.number]),
readOnly:c["default"].PropTypes.bool,disabled:c["default"].PropTypes.bool,placeholder:c["default"].PropTypes.string,type:c["default"].PropTypes.string},g.defaultProps={value:"",extraClass:"",className:"", readOnly:c["default"].PropTypes.bool,disabled:c["default"].PropTypes.bool,placeholder:c["default"].PropTypes.string,type:c["default"].PropTypes.string},g.defaultProps={value:"",extraClass:"",className:"",
type:"text"},t.TextField=g,t["default"]=(0,h["default"])(g)},function(e,t){e.exports=FieldHolder},function(e,t,n){(function(t){e.exports=t.Toolbar=n(137)}).call(t,function(){return this}())},function(e,t,n){ type:"text"},t.TextField=g,t["default"]=(0,h["default"])(g)},function(e,t){e.exports=FieldHolder},function(e,t,n){(function(t){e.exports=t.Toolbar=n(138)}).call(t,function(){return this}())},function(e,t,n){
"use strict" "use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -804,7 +809,7 @@ return u["default"].createElement("div",{className:"toolbar toolbar--north"},u["
}},{key:"handleBackButtonClick",value:function i(e){return"undefined"!=typeof this.props.handleBackButtonClick?void this.props.handleBackButtonClick(e):void e.preventDefault()}}]),t}(d["default"]) }},{key:"handleBackButtonClick",value:function i(e){return"undefined"!=typeof this.props.handleBackButtonClick?void this.props.handleBackButtonClick(e):void e.preventDefault()}}]),t}(d["default"])
f.propTypes={handleBackButtonClick:u["default"].PropTypes.func,showBackButton:u["default"].PropTypes.bool,breadcrumbs:u["default"].PropTypes.array},f.defaultProps={showBackButton:!1},t["default"]=f},function(e,t,n){ f.propTypes={handleBackButtonClick:u["default"].PropTypes.func,showBackButton:u["default"].PropTypes.bool,breadcrumbs:u["default"].PropTypes.array},f.defaultProps={showBackButton:!1},t["default"]=f},function(e,t,n){
(function(t){e.exports=t.Breadcrumb=n(139)}).call(t,function(){return this}())},function(e,t,n){"use strict" (function(t){e.exports=t.Breadcrumb=n(140)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -812,7 +817,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{crumbs:e.breadcrumbs e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function s(e){return{crumbs:e.breadcrumbs
}}Object.defineProperty(t,"__esModule",{value:!0}),t.Breadcrumb=void 0 }}Object.defineProperty(t,"__esModule",{value:!0}),t.Breadcrumb=void 0
var l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(107),h=n(140),m=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(107),h=n(141),m=function(e){
function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),l(t,[{key:"render",value:function n(){return c["default"].createElement("ol",{className:"breadcrumb" function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),l(t,[{key:"render",value:function n(){return c["default"].createElement("ol",{className:"breadcrumb"
},this.getBreadcrumbs())}},{key:"getBreadcrumbs",value:function i(){return"undefined"==typeof this.props.crumbs?null:[].concat(this.props.crumbs.slice(0,-1).map(function(e,t){return[c["default"].createElement("li",{ },this.getBreadcrumbs())}},{key:"getBreadcrumbs",value:function i(){return"undefined"==typeof this.props.crumbs?null:[].concat(this.props.crumbs.slice(0,-1).map(function(e,t){return[c["default"].createElement("li",{
className:"breadcrumb__item"},c["default"].createElement(h.Link,{key:t,className:"breadcrumb__item-title",to:e.href,onClick:e.onClick},e.text))]}),this.props.crumbs.slice(-1).map(function(e,t){var n=["breadcrumb__icon",e.icon?e.icon.className:""].join(" ") className:"breadcrumb__item"},c["default"].createElement(h.Link,{key:t,className:"breadcrumb__item-title",to:e.href,onClick:e.onClick},e.text))]}),this.props.crumbs.slice(-1).map(function(e,t){var n=["breadcrumb__icon",e.icon?e.icon.className:""].join(" ")
@ -820,12 +825,12 @@ className:"breadcrumb__item"},c["default"].createElement(h.Link,{key:t,className
return[c["default"].createElement("li",{className:"breadcrumb__item breadcrumb__item--last"},c["default"].createElement("h2",{className:"breadcrumb__item-title breadcrumb__item-title--last",key:t},e.text,e.icon&&c["default"].createElement("span",{ return[c["default"].createElement("li",{className:"breadcrumb__item breadcrumb__item--last"},c["default"].createElement("h2",{className:"breadcrumb__item-title breadcrumb__item-title--last",key:t},e.text,e.icon&&c["default"].createElement("span",{
className:n,onClick:e.icon.action})))]}))}}]),t}(f["default"]) className:n,onClick:e.icon.action})))]}))}}]),t}(f["default"])
m.propTypes={crumbs:c["default"].PropTypes.array},t.Breadcrumb=m,t["default"]=(0,p.connect)(s)(m)},function(e,t){e.exports=ReactRouter},function(e,t,n){(function(t){e.exports=t.BreadcrumbsActions=n(142) m.propTypes={crumbs:c["default"].PropTypes.array},t.Breadcrumb=m,t["default"]=(0,p.connect)(s)(m)},function(e,t){e.exports=ReactRouter},function(e,t,n){(function(t){e.exports=t.BreadcrumbsActions=n(143)
}).call(t,function(){return this}())},function(e,t,n){"use strict" }).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:a["default"].SET_BREADCRUMBS,payload:{breadcrumbs:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.setBreadcrumbs=r function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:a["default"].SET_BREADCRUMBS,payload:{breadcrumbs:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.setBreadcrumbs=r
var o=n(143),a=i(o)},function(e,t){"use strict" var o=n(144),a=i(o)},function(e,t){"use strict"
Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_BREADCRUMBS:"SET_BREADCRUMBS"}},function(e,t,n){(function(t){e.exports=t.Config=n(145)}).call(t,function(){return this}())},function(e,t){ Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_BREADCRUMBS:"SET_BREADCRUMBS"}},function(e,t,n){(function(t){e.exports=t.Config=n(146)}).call(t,function(){return this}())},function(e,t){
"use strict" "use strict"
function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
@ -833,13 +838,13 @@ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Obj
n(this,e)}return i(e,null,[{key:"get",value:function t(e){return window.ss.config[e]}},{key:"getAll",value:function r(){return window.ss.config}},{key:"getSection",value:function o(e){return window.ss.config.sections[e] n(this,e)}return i(e,null,[{key:"get",value:function t(e){return window.ss.config[e]}},{key:"getAll",value:function r(){return window.ss.config}},{key:"getSection",value:function o(e){return window.ss.config.sections[e]
}}]),e}() }}]),e}()
t["default"]=r},function(e,t,n){(function(t){e.exports=t.ReducerRegister=n(147)}).call(t,function(){return this}())},function(e,t){"use strict" t["default"]=r},function(e,t,n){(function(t){e.exports=t.ReducerRegister=n(148)}).call(t,function(){return this}())},function(e,t){"use strict"
function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var i=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),r={},o=function(){function e(){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),r={},o=function(){function e(){
n(this,e)}return i(e,[{key:"add",value:function t(e,n){if("undefined"!=typeof r[e])throw new Error("Reducer already exists at '"+e+"'") n(this,e)}return i(e,[{key:"add",value:function t(e,n){if("undefined"!=typeof r[e])throw new Error("Reducer already exists at '"+e+"'")
r[e]=n}},{key:"getAll",value:function o(){return r}},{key:"getByKey",value:function a(e){return r[e]}},{key:"remove",value:function s(e){delete r[e]}}]),e}() r[e]=n}},{key:"getAll",value:function o(){return r}},{key:"getByKey",value:function a(e){return r[e]}},{key:"remove",value:function s(e){delete r[e]}}]),e}()
window.ss=window.ss||{},window.ss.reducerRegister=window.ss.reducerRegister||new o,t["default"]=window.ss.reducerRegister},function(e,t,n){(function(t){e.exports=t.ReactRouteRegister=n(149)}).call(t,function(){ window.ss=window.ss||{},window.ss.reducerRegister=window.ss.reducerRegister||new o,t["default"]=window.ss.reducerRegister},function(e,t,n){(function(t){e.exports=t.ReactRouteRegister=n(150)}).call(t,function(){
return this}())},function(e,t){"use strict" return this}())},function(e,t){"use strict"
function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
@ -858,7 +863,7 @@ return t.path===e})
return i<0?null:n.splice(i,1)[0]}}]),e}() return i<0?null:n.splice(i,1)[0]}}]),e}()
window.ss=window.ss||{},window.ss.routeRegister=window.ss.routeRegister||new o,t["default"]=window.ss.routeRegister},function(e,t,n){(function(t){e.exports=t.Injector=n(104)}).call(t,function(){return this window.ss=window.ss||{},window.ss.routeRegister=window.ss.routeRegister||new o,t["default"]=window.ss.routeRegister},function(e,t,n){(function(t){e.exports=t.Injector=n(104)}).call(t,function(){return this
}())},function(e,t,n){(function(t){e.exports=t.Router=n(152)}).call(t,function(){return this}())},function(e,t,n){"use strict" }())},function(e,t,n){(function(t){e.exports=t.Router=n(153)}).call(t,function(){return this}())},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=c["default"].getAbsoluteBase(),n=f["default"].resolve(t,e) function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){var t=c["default"].getAbsoluteBase(),n=f["default"].resolve(t,e)
return 0!==n.indexOf(t)?n:n.substring(t.length-1)}function o(e){return function(t,n,i,r){return e(c["default"].resolveURLToBase(t),n,i,r)}}function a(e){var t=new c["default"].Route(e) return 0!==n.indexOf(t)?n:n.substring(t.length-1)}function o(e){return function(t,n,i,r){return e(c["default"].resolveURLToBase(t),n,i,r)}}function a(e){var t=new c["default"].Route(e)
return t.match(c["default"].current,{})}function s(){return c["default"].absoluteBaseURL}function l(e){c["default"].absoluteBaseURL=e return t.match(c["default"].current,{})}function s(){return c["default"].absoluteBaseURL}function l(e){c["default"].absoluteBaseURL=e
@ -866,7 +871,7 @@ var t=document.createElement("a")
t.href=e t.href=e
var n=t.pathname var n=t.pathname
n=n.replace(/\/$/,""),n.match(/^[^\/]/)&&(n="/"+n),c["default"].base(n)}Object.defineProperty(t,"__esModule",{value:!0}) n=n.replace(/\/$/,""),n.match(/^[^\/]/)&&(n="/"+n),c["default"].base(n)}Object.defineProperty(t,"__esModule",{value:!0})
var u=n(153),c=i(u),d=n(154),f=i(d) var u=n(154),c=i(u),d=n(155),f=i(d)
c["default"].oldshow||(c["default"].oldshow=c["default"].show),c["default"].setAbsoluteBase=l.bind(c["default"]),c["default"].getAbsoluteBase=s.bind(c["default"]),c["default"].resolveURLToBase=r.bind(c["default"]), c["default"].oldshow||(c["default"].oldshow=c["default"].show),c["default"].setAbsoluteBase=l.bind(c["default"]),c["default"].getAbsoluteBase=s.bind(c["default"]),c["default"].resolveURLToBase=r.bind(c["default"]),
c["default"].show=o(c["default"].oldshow),c["default"].routeAppliesToCurrentLocation=a,window.ss=window.ss||{},window.ss.router=window.ss.router||c["default"],t["default"]=window.ss.router},function(e,t){ c["default"].show=o(c["default"].oldshow),c["default"].routeAppliesToCurrentLocation=a,window.ss=window.ss||{},window.ss.router=window.ss.router||c["default"],t["default"]=window.ss.router},function(e,t){
e.exports=Page},function(e,t,n){"use strict" e.exports=Page},function(e,t,n){"use strict"
@ -876,10 +881,10 @@ function i(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,
var r=new i var r=new i
return r.parse(e,t,n),r}function o(e){return u.isString(e)&&(e=r(e)),e instanceof i?e.format():i.prototype.format.call(e)}function a(e,t){return r(e,!1,!0).resolve(t)}function s(e,t){return e?r(e,!1,!0).resolveObject(t):t return r.parse(e,t,n),r}function o(e){return u.isString(e)&&(e=r(e)),e instanceof i?e.format():i.prototype.format.call(e)}function a(e,t){return r(e,!1,!0).resolve(t)}function s(e,t){return e?r(e,!1,!0).resolveObject(t):t
}var l=n(155),u=n(156) }var l=n(156),u=n(157)
t.parse=r,t.resolve=a,t.resolveObject=s,t.format=o,t.Url=i t.parse=r,t.resolve=a,t.resolveObject=s,t.format=o,t.Url=i
var c=/^([a-z0-9.+-]+:)/i,d=/:[0-9]*$/,f=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,p=["<",">",'"',"`"," ","\r","\n","\t"],h=["{","}","|","\\","^","`"].concat(p),m=["'"].concat(h),g=["%","/","?",";","#"].concat(m),v=["/","?","#"],y=255,b=/^[+a-z0-9A-Z_-]{0,63}$/,_=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,w={ var c=/^([a-z0-9.+-]+:)/i,d=/:[0-9]*$/,f=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,p=["<",">",'"',"`"," ","\r","\n","\t"],h=["{","}","|","\\","^","`"].concat(p),m=["'"].concat(h),g=["%","/","?",";","#"].concat(m),v=["/","?","#"],y=255,b=/^[+a-z0-9A-Z_-]{0,63}$/,_=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,w={
javascript:!0,"javascript:":!0},C={javascript:!0,"javascript:":!0},T={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},E=n(157) javascript:!0,"javascript:":!0},C={javascript:!0,"javascript:":!0},T={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},E=n(158)
i.prototype.parse=function(e,t,n){if(!u.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e) i.prototype.parse=function(e,t,n){if(!u.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e)
var i=e.indexOf("?"),r=i!==-1&&i<e.indexOf("#")?"?":"#",o=e.split(r),a=/\\/g var i=e.indexOf("?"),r=i!==-1&&i<e.indexOf("#")?"?":"#",o=e.split(r),a=/\\/g
o[0]=o[0].replace(a,"/"),e=o.join(r) o[0]=o[0].replace(a,"/"),e=o.join(r)
@ -976,7 +981,7 @@ w={version:"1.3.2",ucs2:{decode:u,encode:c},decode:h,encode:m,toASCII:v,toUnicod
},function(e,t){"use strict" },function(e,t){"use strict"
e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,t,n){ e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,t,n){
"use strict" "use strict"
t.decode=t.parse=n(158),t.encode=t.stringify=n(159)},function(e,t){"use strict" t.decode=t.parse=n(159),t.encode=t.stringify=n(160)},function(e,t){"use strict"
function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,i,r){t=t||"&",i=i||"=" function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,t,i,r){t=t||"&",i=i||"="
var o={} var o={}
if("string"!=typeof e||0===e.length)return o if("string"!=typeof e||0===e.length)return o
@ -1022,7 +1027,7 @@ return e.replace(/^#/,"")},cleanHash:function O(e){return u.stripHash(e.replace(
return!(!t.protocol||t.domain===document.domain)},hasProtocol:function k(e){return/^(:?\w+:)/.test(e)}} return!(!t.protocol||t.domain===document.domain)},hasProtocol:function k(e){return/^(:?\w+:)/.test(e)}}
o["default"].path=u},function(e,t,n){(function(e){"use strict" o["default"].path=u},function(e,t,n){(function(e){"use strict"
function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i) function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i)
n(162),r["default"].widget("ssui.button",r["default"].ui.button,{options:{alternate:{icon:null,text:null},showingAlternate:!1},toggleAlternate:function o(){this._trigger("ontogglealternate")!==!1&&(this.options.alternate.icon||this.options.alternate.text)&&(this.options.showingAlternate=!this.options.showingAlternate, n(163),r["default"].widget("ssui.button",r["default"].ui.button,{options:{alternate:{icon:null,text:null},showingAlternate:!1},toggleAlternate:function o(){this._trigger("ontogglealternate")!==!1&&(this.options.alternate.icon||this.options.alternate.text)&&(this.options.showingAlternate=!this.options.showingAlternate,
this.refresh())},_refreshAlternate:function a(){this._trigger("beforerefreshalternate"),(this.options.alternate.icon||this.options.alternate.text)&&(this.options.showingAlternate?(this.element.find(".ui-button-icon-primary").hide(), this.refresh())},_refreshAlternate:function a(){this._trigger("beforerefreshalternate"),(this.options.alternate.icon||this.options.alternate.text)&&(this.options.showingAlternate?(this.element.find(".ui-button-icon-primary").hide(),
this.element.find(".ui-button-text").hide(),this.element.find(".ui-button-icon-alternate").show(),this.element.find(".ui-button-text-alternate").show()):(this.element.find(".ui-button-icon-primary").show(), this.element.find(".ui-button-text").hide(),this.element.find(".ui-button-icon-alternate").show(),this.element.find(".ui-button-text-alternate").show()):(this.element.find(".ui-button-icon-primary").show(),
this.element.find(".ui-button-text").show(),this.element.find(".ui-button-icon-alternate").hide(),this.element.find(".ui-button-text-alternate").hide()),this._trigger("afterrefreshalternate"))},_resetButton:function s(){ this.element.find(".ui-button-text").show(),this.element.find(".ui-button-icon-alternate").hide(),this.element.find(".ui-button-text-alternate").hide()),this._trigger("afterrefreshalternate"))},_resetButton:function s(){
@ -1062,7 +1067,7 @@ o.find("*").add(o).disableSelection()},destroy:function m(){this.element.unbind(
function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e
},_jQuery=__webpack_require__(1),_jQuery2=_interopRequireDefault(_jQuery) },_jQuery=__webpack_require__(1),_jQuery2=_interopRequireDefault(_jQuery)
__webpack_require__(161) __webpack_require__(162)
var windowWidth,windowHeight var windowWidth,windowHeight
_jQuery2["default"].noConflict(),window.ss=window.ss||{},window.ss.debounce=function(e,t,n){var i,r,o,a=function s(){i=null,n||e.apply(r,o)} _jQuery2["default"].noConflict(),window.ss=window.ss||{},window.ss.debounce=function(e,t,n){var i,r,o,a=function s(){i=null,n||e.apply(r,o)}
return function(){var s=n&&!i return function(){var s=n&&!i
@ -1347,7 +1352,7 @@ l&&"#"!=l?(l=l.split("?")[0],t.jstree("deselect_all"),t.jstree("uncheck_all"),e.
s.loadPanel(l)):t.removeForm()}})}}),e(".cms-content .cms-content-fields").entwine({redraw:function r(){window.debug&&console.log("redraw",this.attr("class"),this.get(0))}}),e(".cms-content .cms-content-header, .cms-content .cms-content-actions").entwine({ s.loadPanel(l)):t.removeForm()}})}}),e(".cms-content .cms-content-fields").entwine({redraw:function r(){window.debug&&console.log("redraw",this.attr("class"),this.get(0))}}),e(".cms-content .cms-content-header, .cms-content .cms-content-actions").entwine({
redraw:function o(){window.debug&&console.log("redraw",this.attr("class"),this.get(0)),this.height("auto"),this.height(this.innerHeight()-this.css("padding-top")-this.css("padding-bottom"))}})})},function(e,t,n){ redraw:function o(){window.debug&&console.log("redraw",this.attr("class"),this.get(0)),this.height("auto"),this.height(this.innerHeight()-this.css("padding-top")-this.css("padding-bottom"))}})})},function(e,t,n){
(function(e){"use strict" (function(e){"use strict"
function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(114),a=t(o) function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(115),a=t(o)
window.onbeforeunload=function(e){var t=(0,r["default"])(".cms-edit-form") window.onbeforeunload=function(e){var t=(0,r["default"])(".cms-edit-form")
if(t.trigger("beforesubmitform"),t.is(".changed")&&!t.is(".discardchanges"))return a["default"]._t("LeftAndMain.CONFIRMUNSAVEDSHORT")},r["default"].entwine("ss",function(e){e(".cms-edit-form").entwine({ if(t.trigger("beforesubmitform"),t.is(".changed")&&!t.is(".discardchanges"))return a["default"]._t("LeftAndMain.CONFIRMUNSAVEDSHORT")},r["default"].entwine("ss",function(e){e(".cms-edit-form").entwine({
PlaceholderHtml:"",ChangeTrackerOptions:{ignoreFieldSelector:".no-change-track, .ss-upload :input, .cms-navigator :input"},ValidationErrorShown:!1,onadd:function t(){var e=this PlaceholderHtml:"",ChangeTrackerOptions:{ignoreFieldSelector:".no-change-track, .ss-upload :input, .cms-navigator :input"},ValidationErrorShown:!1,onadd:function t(){var e=this
@ -1442,7 +1447,7 @@ this.toggleCSS(t),this.toggleIndicator(t),this._super()},toggleCSS:function R(e)
void 0===t?e.setPersistedCollapsedState(e.hasClass("collapsed")):void 0!==t&&i===!1&&e.clearPersistedCollapsedState(),e.setPersistedStickyState(i),this.toggleCSS(i),this.toggleIndicator(i),this._super() void 0===t?e.setPersistedCollapsedState(e.hasClass("collapsed")):void 0!==t&&i===!1&&e.clearPersistedCollapsedState(),e.setPersistedStickyState(i),this.toggleCSS(i),this.toggleIndicator(i),this._super()
}})})},function(e,t,n){"use strict" }})})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(114),s=i(a) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(115),s=i(a)
o["default"].entwine("ss.preview",function(e){e(".cms-preview").entwine({AllowedStates:["StageLink","LiveLink","ArchiveLink"],CurrentStateName:null,CurrentSizeName:"auto",IsPreviewEnabled:!1,DefaultMode:"split", o["default"].entwine("ss.preview",function(e){e(".cms-preview").entwine({AllowedStates:["StageLink","LiveLink","ArchiveLink"],CurrentStateName:null,CurrentSizeName:"auto",IsPreviewEnabled:!1,DefaultMode:"split",
Sizes:{auto:{width:"100%",height:"100%"},mobile:{width:"335px",height:"568px"},mobileLandscape:{width:"583px",height:"320px"},tablet:{width:"783px",height:"1024px"},tabletLandscape:{width:"1039px",height:"768px" Sizes:{auto:{width:"100%",height:"100%"},mobile:{width:"335px",height:"568px"},mobileLandscape:{width:"583px",height:"320px"},tablet:{width:"783px",height:"1024px"},tabletLandscape:{width:"1039px",height:"768px"
},desktop:{width:"1024px",height:"800px"}},changeState:function t(n,i){var r=this,o=this._getNavigatorStates() },desktop:{width:"1024px",height:"800px"}},changeState:function t(n,i){var r=this,o=this._getNavigatorStates()
@ -1519,7 +1524,7 @@ e(".cms-preview").changeSize(n)}}),e(".preview-selector select.preview-dropdown"
return"undefined"!=typeof i&&n.removeClass(i),n.addClass(t),n.attr("data-icon",t),this}}),e(".preview-mode-selector .chosen-drop li:last-child").entwine({onmatch:function U(){e(".preview-mode-selector").hasClass("split-disabled")?this.parent().append('<div class="disabled-tooltip"></div>'):this.parent().append('<div class="disabled-tooltip" style="display: none;"></div>') return"undefined"!=typeof i&&n.removeClass(i),n.addClass(t),n.attr("data-icon",t),this}}),e(".preview-mode-selector .chosen-drop li:last-child").entwine({onmatch:function U(){e(".preview-mode-selector").hasClass("split-disabled")?this.parent().append('<div class="disabled-tooltip"></div>'):this.parent().append('<div class="disabled-tooltip" style="display: none;"></div>')
}}),e(".preview-device-outer").entwine({onclick:function L(){this.parent(".preview__device").toggleClass("rotate")}})})},function(e,t,n){(function(e){"use strict" }}),e(".preview-device-outer").entwine({onclick:function L(){this.parent(".preview__device").toggleClass("rotate")}})})},function(e,t,n){(function(e){"use strict"
function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(114),a=t(o) function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(115),a=t(o)
r["default"].entwine("ss.tree",function(t){t("#Form_BatchActionsForm").entwine({Actions:[],getTree:function n(){return t(".cms-tree")},fromTree:{oncheck_node:function i(e,t){this.serializeFromTree()},onuncheck_node:function r(e,t){ r["default"].entwine("ss.tree",function(t){t("#Form_BatchActionsForm").entwine({Actions:[],getTree:function n(){return t(".cms-tree")},fromTree:{oncheck_node:function i(e,t){this.serializeFromTree()},onuncheck_node:function r(e,t){
this.serializeFromTree()}},onmatch:function o(){var e=this this.serializeFromTree()}},onmatch:function o(){var e=this
e.getTree().bind("load_node.jstree",function(t,n){e.refreshSelected()})},onunmatch:function s(){var e=this e.getTree().bind("load_node.jstree",function(t,n){e.refreshSelected()})},onunmatch:function s(){var e=this
@ -1587,7 +1592,7 @@ this.addClass("description-toggle-enabled"),n.on("click",function(){i[e?"hide":"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
o["default"].entwine("ss",function(e){e(".TreeDropdownField").entwine({"from .cms-container form":{onaftersubmitform:function t(e){this.find(".tree-holder").empty(),this._super()}}})})},function(e,t,n){ o["default"].entwine("ss",function(e){e(".TreeDropdownField").entwine({"from .cms-container form":{onaftersubmitform:function t(e){this.find(".tree-holder").empty(),this._super()}}})})},function(e,t,n){
"use strict" "use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(5),s=i(a),l=n(176),u=i(l),c=n(107),d=n(177),f=i(d) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(5),s=i(a),l=n(177),u=i(l),c=n(107),d=n(178),f=i(d)
o["default"].entwine("ss",function(e){e(".cms-content-actions .add-to-campaign-action,#add-to-campaign__action").entwine({onclick:function t(){var t=e("#add-to-campaign__dialog-wrapper") o["default"].entwine("ss",function(e){e(".cms-content-actions .add-to-campaign-action,#add-to-campaign__action").entwine({onclick:function t(){var t=e("#add-to-campaign__dialog-wrapper")
return t.length||(t=e('<div id="add-to-campaign__dialog-wrapper" />'),e("body").append(t)),t.open(),!1}}),e("#add-to-campaign__dialog-wrapper").entwine({onunmatch:function n(){this._clearModal()},open:function i(){ return t.length||(t=e('<div id="add-to-campaign__dialog-wrapper" />'),e("body").append(t)),t.open(),!1}}),e("#add-to-campaign__dialog-wrapper").entwine({onunmatch:function n(){this._clearModal()},open:function i(){
this._renderModal(!0)},close:function r(){this._renderModal(!1)},_renderModal:function o(t){var n=this,i=function h(){return n.close()},r=function m(){return n._handleSubmitModal.apply(n,arguments)},o=e("form.cms-edit-form :input[name=ID]").val(),a=window.ss.store,l="SilverStripe\\CMS\\Controllers\\CMSPageEditController",d=a.getState().config.sections[l],p=d.form.AddToCampaignForm.schemaUrl+"/"+o this._renderModal(!0)},close:function r(){this._renderModal(!1)},_renderModal:function o(t){var n=this,i=function h(){return n.close()},r=function m(){return n._handleSubmitModal.apply(n,arguments)},o=e("form.cms-edit-form :input[name=ID]").val(),a=window.ss.store,l="SilverStripe\\CMS\\Controllers\\CMSPageEditController",d=a.getState().config.sections[l],p=d.form.AddToCampaignForm.schemaUrl+"/"+o
@ -1597,7 +1602,7 @@ u["default"].render(s["default"].createElement(c.Provider,{store:a},s["default"]
responseClassGood:"modal__response modal__response--good"})),this[0])},_clearModal:function a(){u["default"].unmountComponentAtNode(this[0])},_handleSubmitModal:function l(e,t,n){return n()}})})},,function(e,t){ responseClassGood:"modal__response modal__response--good"})),this[0])},_clearModal:function a(){u["default"].unmountComponentAtNode(this[0])},_handleSubmitModal:function l(e,t,n){return n()}})})},,function(e,t){
e.exports=FormBuilderModal},function(e,t,n){"use strict" e.exports=FormBuilderModal},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
n(163),n(179) n(164),n(180)
var a=function s(e){var t=(0,o["default"])((0,o["default"])(this).contents()).find(".message") var a=function s(e){var t=(0,o["default"])((0,o["default"])(this).contents()).find(".message")
if(t&&t.html()){var n=(0,o["default"])(window.parent.document).find("#Form_EditForm_Members").get(0) if(t&&t.html()){var n=(0,o["default"])(window.parent.document).find("#Form_EditForm_Members").get(0)
n&&n.refresh() n&&n.refresh()
@ -1625,7 +1630,7 @@ e(this).prop("checked","checked")}):t.each(function(){e(this).prop("checked",e(t
})}})})},function(e,t,n){"use strict" })}})})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
n(163),o["default"].entwine("ss",function(e){e(".cms-content-tools #Form_SearchForm").entwine({onsubmit:function t(e){this.trigger("beforeSubmit")}}),e(".importSpec").entwine({onmatch:function n(){this.find("div.details").hide(), n(164),o["default"].entwine("ss",function(e){e(".cms-content-tools #Form_SearchForm").entwine({onsubmit:function t(e){this.trigger("beforeSubmit")}}),e(".importSpec").entwine({onmatch:function n(){this.find("div.details").hide(),
this.find("a.detailsLink").click(function(){return e("#"+e(this).attr("href").replace(/.*#/,"")).slideToggle(),!1}),this._super()},onunmatch:function i(){this._super()}})})},function(e,t,n){"use strict" this.find("a.detailsLink").click(function(){return e("#"+e(this).attr("href").replace(/.*#/,"")).slideToggle(),!1}),this._super()},onunmatch:function i(){this._super()}})})},function(e,t,n){"use strict"
@ -1640,8 +1645,8 @@ t.toggleClass("active"),t.find(".toggle-content").css("minHeight",n)}})},functio
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r);(0,o["default"])(document).on("click",".confirmedpassword .showOnClick a",function(){var e=(0,o["default"])(".showOnClickContainer",(0, function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r);(0,o["default"])(document).on("click",".confirmedpassword .showOnClick a",function(){var e=(0,o["default"])(".showOnClickContainer",(0,
o["default"])(this).parent()) o["default"])(this).parent())
return e.toggle("fast",function(){e.find('input[type="hidden"]').val(e.is(":visible")?1:0)}),!1})},function(e,t,n){"use strict" return e.toggle("fast",function(){e.find('input[type="hidden"]').val(e.is(":visible")?1:0)}),!1})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(114),s=i(a) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(115),s=i(a)
window.tmpl=n(184),n(185),n(186),o["default"].widget("blueimpUIX.fileupload",o["default"].blueimpUI.fileupload,{_initTemplates:function l(){this.options.templateContainer=document.createElement(this._files.prop("nodeName")), window.tmpl=n(185),n(186),n(187),o["default"].widget("blueimpUIX.fileupload",o["default"].blueimpUI.fileupload,{_initTemplates:function l(){this.options.templateContainer=document.createElement(this._files.prop("nodeName")),
this.options.uploadTemplate=window.tmpl(this.options.uploadTemplateName),this.options.downloadTemplate=window.tmpl(this.options.downloadTemplateName)},_enableFileInputButton:function u(){o["default"].blueimpUI.fileupload.prototype._enableFileInputButton.call(this), this.options.uploadTemplate=window.tmpl(this.options.uploadTemplateName),this.options.downloadTemplate=window.tmpl(this.options.downloadTemplateName)},_enableFileInputButton:function u(){o["default"].blueimpUI.fileupload.prototype._enableFileInputButton.call(this),
this.element.find(".ss-uploadfield-addfile").show()},_disableFileInputButton:function c(){o["default"].blueimpUI.fileupload.prototype._disableFileInputButton.call(this),this.element.find(".ss-uploadfield-addfile").hide() this.element.find(".ss-uploadfield-addfile").show()},_disableFileInputButton:function c(){o["default"].blueimpUI.fileupload.prototype._disableFileInputButton.call(this),this.element.find(".ss-uploadfield-addfile").hide()
@ -1751,22 +1756,22 @@ t.length&&t.removeClass("selected")
var n=e.nextAll("li.selected") var n=e.nextAll("li.selected")
n.length&&n.removeClass("selected"),(0,o["default"])(this).focus()})})},function(e,t,n){"use strict" n.length&&n.removeClass("selected"),(0,o["default"])(this).focus()})})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
n(162),o["default"].fn.extend({ssDatepicker:function a(e){return(0,o["default"])(this).each(function(){if(!((0,o["default"])(this).prop("disabled")||(0,o["default"])(this).prop("readonly")||(0,o["default"])(this).data("datepicker"))){ n(163),o["default"].fn.extend({ssDatepicker:function a(e){return(0,o["default"])(this).each(function(){if(!((0,o["default"])(this).prop("disabled")||(0,o["default"])(this).prop("readonly")||(0,o["default"])(this).data("datepicker"))){
(0,o["default"])(this).siblings("button").addClass("ui-icon ui-icon-calendar") (0,o["default"])(this).siblings("button").addClass("ui-icon ui-icon-calendar")
var t=(0,o["default"])(this).closest(".field.date"),n=o["default"].extend(e||{},(0,o["default"])(this).data(),(0,o["default"])(this).data("jqueryuiconfig"),{}) var t=(0,o["default"])(this).closest(".field.date"),n=o["default"].extend(e||{},(0,o["default"])(this).data(),(0,o["default"])(this).data("jqueryuiconfig"),{})
n.showcalendar&&(n.locale&&o["default"].datepicker.regional[n.locale]&&(n=o["default"].extend(n,o["default"].datepicker.regional[n.locale],{})),n.min&&(n.minDate=o["default"].datepicker.parseDate("yy-mm-dd",n.min)), n.showcalendar&&(n.locale&&o["default"].datepicker.regional[n.locale]&&(n=o["default"].extend(n,o["default"].datepicker.regional[n.locale],{})),n.min&&(n.minDate=o["default"].datepicker.parseDate("yy-mm-dd",n.min)),
n.max&&(n.maxDate=o["default"].datepicker.parseDate("yy-mm-dd",n.max)),n.dateFormat=n.jquerydateformat,(0,o["default"])(this).datepicker(n))}})}}),(0,o["default"])(document).on("click",".field.date input.text,input.text.date",function(){ n.max&&(n.maxDate=o["default"].datepicker.parseDate("yy-mm-dd",n.max)),n.dateFormat=n.jquerydateformat,(0,o["default"])(this).datepicker(n))}})}}),(0,o["default"])(document).on("click",".field.date input.text,input.text.date",function(){
(0,o["default"])(this).ssDatepicker(),(0,o["default"])(this).data("datepicker")&&(0,o["default"])(this).datepicker("show")})},function(e,t,n){"use strict" (0,o["default"])(this).ssDatepicker(),(0,o["default"])(this).data("datepicker")&&(0,o["default"])(this).datepicker("show")})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
n(162),o["default"].entwine("ss",function(e){e(".ss-toggle").entwine({onadd:function t(){this._super(),this.accordion({heightStyle:"content",collapsible:!0,active:!this.hasClass("ss-toggle-start-closed")&&0 n(163),o["default"].entwine("ss",function(e){e(".ss-toggle").entwine({onadd:function t(){this._super(),this.accordion({heightStyle:"content",collapsible:!0,active:!this.hasClass("ss-toggle-start-closed")&&0
})},onremove:function n(){this.data("accordion")&&this.accordion("destroy"),this._super()},getTabSet:function i(){return this.closest(".ss-tabset")},fromTabSet:{ontabsshow:function r(){this.accordion("resize") })},onremove:function n(){this.data("accordion")&&this.accordion("destroy"),this._super()},getTabSet:function i(){return this.closest(".ss-tabset")},fromTabSet:{ontabsshow:function r(){this.accordion("resize")
}}})})},function(e,t,n){"use strict" }}})})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
o["default"].entwine("ss",function(e){e(".memberdatetimeoptionset").entwine({onmatch:function t(){this.find(".toggle-content").hide(),this._super()}}),e(".memberdatetimeoptionset .toggle").entwine({onclick:function n(t){ o["default"].entwine("ss",function(e){e(".memberdatetimeoptionset").entwine({onmatch:function t(){this.find(".toggle-content").hide(),this._super()}}),e(".memberdatetimeoptionset .toggle").entwine({onclick:function n(t){
return e(this).closest(".form__field-description").parent().find(".toggle-content").toggle(),!1}})})},function(e,t,n){(function(e){"use strict" return e(this).closest(".form__field-description").parent().find(".toggle-content").toggle(),!1}})})},function(e,t,n){(function(e){"use strict"
function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(114),a=t(o) function t(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),r=t(i),o=n(115),a=t(o)
n(192),n(193),r["default"].entwine("ss",function(t){var n,i n(193),n(194),r["default"].entwine("ss",function(t){var n,i
t(window).bind("resize.treedropdownfield",function(){var e=function a(){t(".TreeDropdownField").closePanel()} t(window).bind("resize.treedropdownfield",function(){var e=function a(){t(".TreeDropdownField").closePanel()}
if(t.browser.msie&&parseInt(t.browser.version,10)<9){var r=t(window).width(),o=t(window).height() if(t.browser.msie&&parseInt(t.browser.version,10)<9){var r=t(window).width(),o=t(window).height()
r==n&&o==i||(n=r,i=o,e())}else e()}) r==n&&o==i||(n=r,i=o,e())}else e()})
@ -1834,7 +1839,7 @@ onadd:function M(){this._super(),this.bind("change.TreeDropdownField",function()
},,,function(module,exports,__webpack_require__){"use strict" },,,function(module,exports,__webpack_require__){"use strict"
function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},_jQuery=__webpack_require__(1),_jQuery2=_interopRequireDefault(_jQuery),_i18n=__webpack_require__(114),_i18n2=_interopRequireDefault(_i18n),_react=__webpack_require__(5),_react2=_interopRequireDefault(_react),_reactDom=__webpack_require__(176),_reactDom2=_interopRequireDefault(_reactDom),_reactRedux=__webpack_require__(107),ss="undefined"!=typeof window.ss?window.ss:{} for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},_jQuery=__webpack_require__(1),_jQuery2=_interopRequireDefault(_jQuery),_i18n=__webpack_require__(115),_i18n2=_interopRequireDefault(_i18n),_react=__webpack_require__(5),_react2=_interopRequireDefault(_react),_reactDom=__webpack_require__(177),_reactDom2=_interopRequireDefault(_reactDom),_reactRedux=__webpack_require__(107),ss="undefined"!=typeof window.ss?window.ss:{}
ss.editorWrappers={},ss.editorWrappers.tinyMCE=function(){var editorID ss.editorWrappers={},ss.editorWrappers.tinyMCE=function(){var editorID
@ -2072,7 +2077,7 @@ return a&&a.not(c).length&&a.replaceWith(c),l&&l.prepend(s),a||(n.repaint(),n.in
e.noticeAdd({text:i,type:n,stayTime:5e3,inEffect:{left:"0",opacity:"show"}})}})})},function(e,t,n){"use strict" e.noticeAdd({text:i,type:n,stayTime:5e3,inEffect:{left:"0",opacity:"show"}})}})})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r)
n(162),n(196),n(192),o["default"].entwine("ss",function(e){e(".ss-tabset").entwine({IgnoreTabState:!1,onadd:function t(){var e=window.location.hash n(163),n(197),n(193),o["default"].entwine("ss",function(e){e(".ss-tabset").entwine({IgnoreTabState:!1,onadd:function t(){var e=window.location.hash
this.redrawTabs(),""!==e&&this.openTabFromURL(e),this._super()},onremove:function n(){this.data("tabs")&&this.tabs("destroy"),this._super()},redrawTabs:function i(){this.rewriteHashlinks(),this.tabs()}, this.redrawTabs(),""!==e&&this.openTabFromURL(e),this._super()},onremove:function n(){this.data("tabs")&&this.tabs("destroy"),this._super()},redrawTabs:function i(){this.rewriteHashlinks(),this.tabs()},
openTabFromURL:function r(t){var n openTabFromURL:function r(t){var n
e.each(this.find(".ui-tabs-anchor"),function(){if(this.href.indexOf(t)!==-1&&1===e(t).length)return n=e(this),!1}),void 0!==n&&e(document).ready("ajaxComplete",function(){n.click()})},rewriteHashlinks:function o(){ e.each(this.find(".ui-tabs-anchor"),function(){if(this.href.indexOf(t)!==-1&&1===e(t).length)return n=e(this),!1}),void 0!==n&&e(document).ready("ajaxComplete",function(){n.click()})},rewriteHashlinks:function o(){
@ -2080,8 +2085,8 @@ e(this).find("ul a").each(function(){if(e(this).attr("href")){var t=e(this).attr
t&&e(this).attr("href",document.location.href.replace(/#.*/,"")+t[0])}})}}),e(".ui-tabs-active .ui-tabs-anchor").entwine({onmatch:function a(){this.addClass("nav-link active")},onunmatch:function s(){this.removeClass("active") t&&e(this).attr("href",document.location.href.replace(/#.*/,"")+t[0])}})}}),e(".ui-tabs-active .ui-tabs-anchor").entwine({onmatch:function a(){this.addClass("nav-link active")},onunmatch:function s(){this.removeClass("active")
}})})},,function(e,t,n){"use strict" }})})},,function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(114),s=i(a) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(1),o=i(r),a=n(115),s=i(a)
n(162),n(192),o["default"].entwine("ss",function(e){e(".grid-field").entwine({reload:function t(n,i){var r=this,o=this.closest("form"),a=this.find(":input:focus").attr("name"),l=o.find(":input").serializeArray() n(163),n(193),o["default"].entwine("ss",function(e){e(".grid-field").entwine({reload:function t(n,i){var r=this,o=this.closest("form"),a=this.find(":input:focus").attr("name"),l=o.find(":input").serializeArray()
n||(n={}),n.data||(n.data=[]),n.data=n.data.concat(l),window.location.search&&(n.data=window.location.search.replace(/^\?/,"")+"&"+e.param(n.data)),o.addClass("loading"),e.ajax(e.extend({},{headers:{"X-Pjax":"CurrentField" n||(n={}),n.data||(n.data=[]),n.data=n.data.concat(l),window.location.search&&(n.data=window.location.search.replace(/^\?/,"")+"&"+e.param(n.data)),o.addClass("loading"),e.ajax(e.extend({},{headers:{"X-Pjax":"CurrentField"
@ -2148,13 +2153,13 @@ var e={},t=(0,l.combineReducers)(g["default"].getAll()),n=[c["default"]],i=h["de
var p=o(l.createStore),m=p(t,e) var p=o(l.createStore),m=p(t,e)
m.dispatch(y.setConfig(h["default"].getAll())),window.ss=window.ss||{},window.ss.store=m m.dispatch(y.setConfig(h["default"].getAll())),window.ss=window.ss||{},window.ss.store=m
var v=new s["default"](m) var v=new s["default"](m)
v.start(window.location.pathname)}var a=n(199),s=r(a),l=n(108),u=n(221),c=r(u),d=n(109),f=n(220),p=n(145),h=r(p),m=n(222),g=r(m),v=n(223),y=i(v),b=n(225),_=r(b),w=n(227),C=r(w),T=n(228),E=r(T),P=n(229),O=r(P),S=n(231),k=r(S),j=n(232),x=r(j),R=n(248),I=r(R),A=n(10),D=r(A) v.start(window.location.pathname)}var a=n(200),s=r(a),l=n(108),u=n(222),c=r(u),d=n(110),f=n(221),p=n(146),h=r(p),m=n(223),g=r(m),v=n(224),y=i(v),b=n(226),_=r(b),w=n(227),C=r(w),T=n(228),E=r(T),P=n(229),O=r(P),S=n(231),k=r(S),j=n(232),x=r(j),R=n(248),I=r(R),A=n(10),D=r(A)
D["default"].polyfill(),window.onload=o},function(e,t,n){"use strict" D["default"].polyfill(),window.onload=o},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var o=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var o=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),a=n(1),s=i(a),l=n(5),u=i(l),c=n(176),d=i(c),f=n(107),p=n(140),h=n(200),m=i(h),g=n(145),v=i(g),y=n(217),b=i(y),_=n(218),w=i(_),C=n(219),T=i(C),E=n(220),P=function(){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),a=n(1),s=i(a),l=n(5),u=i(l),c=n(177),d=i(c),f=n(107),p=n(141),h=n(201),m=i(h),g=n(146),v=i(g),y=n(218),b=i(y),_=n(219),w=i(_),C=n(220),T=i(C),E=n(221),P=function(){
function e(t){r(this,e),this.store=t function e(t){r(this,e),this.store=t
var n=v["default"].get("absoluteBaseUrl") var n=v["default"].get("absoluteBaseUrl")
b["default"].setAbsoluteBase(n)}return o(e,[{key:"start",value:function t(e){this.matchesLegacyRoute(e)?this.initLegacyRouter():this.initReactRouter()}},{key:"matchesLegacyRoute",value:function n(e){var t=v["default"].get("sections"),n=b["default"].resolveURLToBase(e).replace(/\/$/,"") b["default"].setAbsoluteBase(n)}return o(e,[{key:"start",value:function t(e){this.matchesLegacyRoute(e)?this.initLegacyRouter():this.initReactRouter()}},{key:"matchesLegacyRoute",value:function n(e){var t=v["default"].get("sections"),n=b["default"].resolveURLToBase(e).replace(/\/$/,"")
@ -2186,25 +2191,25 @@ function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).app
return e}}]),t}(d["default"]) return e}}]),t}(d["default"])
t["default"]=f},function(e,t){e.exports=ReactRouterRedux},function(e,t){e.exports=ReduxThunk},function(e,t){e.exports=ReducerRegister},function(e,t,n){"use strict" t["default"]=f},function(e,t){e.exports=ReactRouterRedux},function(e,t){e.exports=ReduxThunk},function(e,t){e.exports=ReducerRegister},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:a["default"].SET_CONFIG,payload:{config:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.setConfig=r function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:a["default"].SET_CONFIG,payload:{config:e}}}Object.defineProperty(t,"__esModule",{value:!0}),t.setConfig=r
var o=n(224),a=i(o)},function(e,t){"use strict" var o=n(225),a=i(o)},function(e,t){"use strict"
Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_CONFIG:"SET_CONFIG"}},function(e,t,n){"use strict" Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_CONFIG:"SET_CONFIG"}},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],t=arguments[1] function i(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],t=arguments[1]
switch(t.type){case u["default"].SET_CONFIG:return(0,s["default"])(o({},e,t.payload.config)) switch(t.type){case u["default"].SET_CONFIG:return(0,s["default"])(o({},e,t.payload.config))
default:return e}}Object.defineProperty(t,"__esModule",{value:!0}) default:return e}}Object.defineProperty(t,"__esModule",{value:!0})
var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(226),s=i(a),l=n(224),u=i(l) for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(109),s=i(a),l=n(225),u=i(l)
t["default"]=r},function(e,t){e.exports=DeepFreezeStrict},function(e,t,n){"use strict" t["default"]=r},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(){var e=arguments.length<=0||void 0===arguments[0]?d:arguments[0],t=arguments.length<=1||void 0===arguments[1]?null:arguments[1] function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(){var e=arguments.length<=0||void 0===arguments[0]?d:arguments[0],t=arguments.length<=1||void 0===arguments[1]?null:arguments[1]
switch(t.type){case c["default"].SET_SCHEMA:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],{id:t.payload.id,schema:t.payload.schema,state:t.payload.state})))) switch(t.type){case c["default"].SET_SCHEMA:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],t.payload))))
case c["default"].SET_SCHEMA_STATE_OVERRIDES:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],{stateOverride:t.payload.stateOverride})))) case c["default"].SET_SCHEMA_STATE_OVERRIDES:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],{stateOverride:t.payload.stateOverride}))))
case c["default"].SET_SCHEMA_LOADING:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],{metadata:a({},e[t.payload.id]&&e[t.payload.id].metadata,{loading:t.payload.loading})})))) case c["default"].SET_SCHEMA_LOADING:return(0,l["default"])(a({},e,r({},t.payload.id,a({},e[t.payload.id],{metadata:a({},e[t.payload.id]&&e[t.payload.id].metadata,{loading:t.payload.loading})}))))
default:return e}}Object.defineProperty(t,"__esModule",{value:!0}) default:return e}}Object.defineProperty(t,"__esModule",{value:!0})
var a=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var a=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e} for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e}
t["default"]=o t["default"]=o
var s=n(226),l=i(s),u=n(33),c=i(u),d=(0,l["default"])({})},function(e,t,n){"use strict" var s=n(109),l=i(s),u=n(33),c=i(u),d=(0,l["default"])({})},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(){var e=arguments.length<=0||void 0===arguments[0]?d:arguments[0],t=arguments[1],n=null,i=null,o=null function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(){var e=arguments.length<=0||void 0===arguments[0]?d:arguments[0],t=arguments[1],n=null,i=null,o=null
@ -2226,7 +2231,7 @@ case c["default"].DELETE_RECORD_SUCCESS:return i=t.payload.recordType,n=e[i],n=O
default:return e}}Object.defineProperty(t,"__esModule",{value:!0}) default:return e}}Object.defineProperty(t,"__esModule",{value:!0})
var a=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var a=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},s=n(226),l=i(s),u=n(125),c=i(u),d={} for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},s=n(109),l=i(s),u=n(126),c=i(u),d={}
t["default"]=o},function(e,t,n){"use strict" t["default"]=o},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],t=arguments[1] function i(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=arguments.length<=0||void 0===arguments[0]?c:arguments[0],t=arguments[1]
switch(t.type){case u["default"].SET_CAMPAIGN_SELECTED_CHANGESETITEM:return(0,s["default"])(o({},e,{changeSetItemId:t.payload.changeSetItemId})) switch(t.type){case u["default"].SET_CAMPAIGN_SELECTED_CHANGESETITEM:return(0,s["default"])(o({},e,{changeSetItemId:t.payload.changeSetItemId}))
@ -2235,7 +2240,7 @@ case u["default"].PUBLISH_CAMPAIGN_REQUEST:return(0,s["default"])(o({},e,{isPubl
case u["default"].PUBLISH_CAMPAIGN_SUCCESS:case u["default"].PUBLISH_CAMPAIGN_FAILURE:return(0,s["default"])(o({},e,{isPublishing:!1})) case u["default"].PUBLISH_CAMPAIGN_SUCCESS:case u["default"].PUBLISH_CAMPAIGN_FAILURE:return(0,s["default"])(o({},e,{isPublishing:!1}))
default:return e}}Object.defineProperty(t,"__esModule",{value:!0}) default:return e}}Object.defineProperty(t,"__esModule",{value:!0})
var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(226),s=i(a),l=n(230),u=i(l),c=(0,s["default"])({campaignId:null,changeSetItemId:null,isPublishing:!1,view:null}) for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(109),s=i(a),l=n(230),u=i(l),c=(0,s["default"])({campaignId:null,changeSetItemId:null,isPublishing:!1,view:null})
t["default"]=r},function(e,t){"use strict" t["default"]=r},function(e,t){"use strict"
Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_CAMPAIGN_ACTIVE_CHANGESET:"SET_CAMPAIGN_ACTIVE_CHANGESET",SET_CAMPAIGN_SELECTED_CHANGESETITEM:"SET_CAMPAIGN_SELECTED_CHANGESETITEM",PUBLISH_CAMPAIGN_REQUEST:"PUBLISH_CAMPAIGN_REQUEST", Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={SET_CAMPAIGN_ACTIVE_CHANGESET:"SET_CAMPAIGN_ACTIVE_CHANGESET",SET_CAMPAIGN_SELECTED_CHANGESETITEM:"SET_CAMPAIGN_SELECTED_CHANGESETITEM",PUBLISH_CAMPAIGN_REQUEST:"PUBLISH_CAMPAIGN_REQUEST",
PUBLISH_CAMPAIGN_SUCCESS:"PUBLISH_CAMPAIGN_SUCCESS",PUBLISH_CAMPAIGN_FAILURE:"PUBLISH_CAMPAIGN_FAILURE"}},function(e,t,n){"use strict" PUBLISH_CAMPAIGN_SUCCESS:"PUBLISH_CAMPAIGN_SUCCESS",PUBLISH_CAMPAIGN_FAILURE:"PUBLISH_CAMPAIGN_FAILURE"}},function(e,t,n){"use strict"
@ -2243,11 +2248,11 @@ function i(e){return e&&e.__esModule?e:{"default":e}}function r(){var e=argument
switch(t.type){case u["default"].SET_BREADCRUMBS:return(0,s["default"])(o([],t.payload.breadcrumbs)) switch(t.type){case u["default"].SET_BREADCRUMBS:return(0,s["default"])(o([],t.payload.breadcrumbs))
default:return e}}Object.defineProperty(t,"__esModule",{value:!0}) default:return e}}Object.defineProperty(t,"__esModule",{value:!0})
var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(226),s=i(a),l=n(143),u=i(l),c=(0,s["default"])([]) for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},a=n(109),s=i(a),l=n(144),u=i(l),c=(0,s["default"])([])
t["default"]=r},function(e,t,n){"use strict" t["default"]=r},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}) function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0})
var o=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var o=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),a=n(104),s=i(a),l=n(134),u=i(l),c=n(132),d=i(c),f=n(233),p=i(f),h=n(235),m=i(h),g=n(236),v=i(g),y=n(237),b=i(y),_=n(238),w=i(_),C=n(239),T=i(C),E=n(240),P=i(E),O=n(241),S=i(O),k=n(242),j=i(k),x=n(243),R=i(x),I=n(244),A=i(I),D=n(245),F=i(D),M=n(246),N=i(M),U=n(247),L=i(U),H=function(){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),a=n(104),s=i(a),l=n(135),u=i(l),c=n(133),d=i(c),f=n(233),p=i(f),h=n(235),m=i(h),g=n(236),v=i(g),y=n(237),b=i(y),_=n(238),w=i(_),C=n(239),T=i(C),E=n(240),P=i(E),O=n(241),S=i(O),k=n(242),j=i(k),x=n(243),R=i(x),I=n(244),A=i(I),D=n(245),F=i(D),M=n(246),N=i(M),U=n(247),L=i(U),H=function(){
function e(){r(this,e)}return o(e,[{key:"start",value:function t(){s["default"].register("TextField",u["default"]),s["default"].register("HiddenField",d["default"]),s["default"].register("CheckboxField",p["default"]), function e(){r(this,e)}return o(e,[{key:"start",value:function t(){s["default"].register("TextField",u["default"]),s["default"].register("HiddenField",d["default"]),s["default"].register("CheckboxField",p["default"]),
s["default"].register("CheckboxSetField",m["default"]),s["default"].register("OptionsetField",v["default"]),s["default"].register("GridField",b["default"]),s["default"].register("SingleSelectField",w["default"]), s["default"].register("CheckboxSetField",m["default"]),s["default"].register("OptionsetField",v["default"]),s["default"].register("GridField",b["default"]),s["default"].register("SingleSelectField",w["default"]),
s["default"].register("PopoverField",T["default"]),s["default"].register("HeaderField",P["default"]),s["default"].register("LiteralField",S["default"]),s["default"].register("HtmlReadonlyField",j["default"]), s["default"].register("PopoverField",T["default"]),s["default"].register("HeaderField",P["default"]),s["default"].register("LiteralField",S["default"]),s["default"].register("HtmlReadonlyField",j["default"]),
@ -2263,7 +2268,7 @@ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,wri
value:!0}) value:!0})
var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(234),f=i(d),p=n(135),h=i(p),m=n(21),g=i(m),v=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(234),f=i(d),p=n(136),h=i(p),m=n(21),g=i(m),v=function(e){
function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),l(t,[{key:"render",value:function n(){var e=(0,h["default"])(f["default"]) function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),l(t,[{key:"render",value:function n(){var e=(0,h["default"])(f["default"])
return c["default"].createElement(e,s({},this.props,{type:"checkbox",hideLabels:!0}))}}]),t}(g["default"]) return c["default"].createElement(e,s({},this.props,{type:"checkbox",hideLabels:!0}))}}]),t}(g["default"])
t["default"]=v},function(e,t,n){"use strict" t["default"]=v},function(e,t,n){"use strict"
@ -2297,7 +2302,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}),t.CheckboxSetField=void 0 value:!0}),t.CheckboxSetField=void 0
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(234),p=i(f),h=n(135),m=i(h),g=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(234),p=i(f),h=n(136),m=i(h),g=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.getItemKey=n.getItemKey.bind(n),n.getOptionProps=n.getOptionProps.bind(n),n.handleChange=n.handleChange.bind(n),n.getValues=n.getValues.bind(n),n}return a(t,e),s(t,[{key:"getItemKey",value:function n(e,t){ return n.getItemKey=n.getItemKey.bind(n),n.getOptionProps=n.getOptionProps.bind(n),n.handleChange=n.handleChange.bind(n),n.getValues=n.getValues.bind(n),n}return a(t,e),s(t,[{key:"getItemKey",value:function n(e,t){
@ -2321,7 +2326,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}),t.OptionsetField=void 0 value:!0}),t.OptionsetField=void 0
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(234),p=i(f),h=n(135),m=i(h),g=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(234),p=i(f),h=n(136),m=i(h),g=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.getItemKey=n.getItemKey.bind(n),n.getOptionProps=n.getOptionProps.bind(n),n.handleChange=n.handleChange.bind(n),n}return a(t,e),s(t,[{key:"getItemKey",value:function n(e,t){return this.props.id+"-"+(e.value||"empty"+t) return n.getItemKey=n.getItemKey.bind(n),n.getOptionProps=n.getOptionProps.bind(n),n.handleChange=n.handleChange.bind(n),n}return a(t,e),s(t,[{key:"getItemKey",value:function n(e,t){return this.props.id+"-"+(e.value||"empty"+t)
@ -2344,7 +2349,7 @@ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,wri
value:!0}),t.SingleSelectField=void 0 value:!0}),t.SingleSelectField=void 0
var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(135),h=i(p),m=n(114),g=i(m),v=n(22),y=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(136),h=i(p),m=n(115),g=i(m),v=n(22),y=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handleChange=n.handleChange.bind(n),n}return a(t,e),l(t,[{key:"render",value:function n(){var e=null return n.handleChange=n.handleChange.bind(n),n}return a(t,e),l(t,[{key:"render",value:function n(){var e=null
@ -2420,7 +2425,7 @@ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,wri
value:!0}),t.HtmlReadonlyField=void 0 value:!0}),t.HtmlReadonlyField=void 0
var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},l=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(135),h=i(p),m=n(22),g=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(5),c=i(u),d=n(21),f=i(d),p=n(136),h=i(p),m=n(22),g=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.getContent=n.getContent.bind(n),n}return a(t,e),l(t,[{key:"getContent",value:function n(){return{__html:this.props.value}}},{key:"getInputProps",value:function i(){return{bsClass:this.props.bsClass, return n.getContent=n.getContent.bind(n),n}return a(t,e),l(t,[{key:"getContent",value:function n(){return{__html:this.props.value}}},{key:"getInputProps",value:function i(){return{bsClass:this.props.bsClass,
@ -2435,7 +2440,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}),t.LookupField=void 0 value:!0}),t.LookupField=void 0
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(22),p=n(135),h=i(p),m=n(114),g=i(m),v=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(22),p=n(136),h=i(p),m=n(115),g=i(m),v=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.getValueCSV=n.getValueCSV.bind(n),n}return a(t,e),s(t,[{key:"getValueCSV",value:function n(){var e=this,t=this.props.value return n.getValueCSV=n.getValueCSV.bind(n),n}return a(t,e),s(t,[{key:"getValueCSV",value:function n(){var e=this,t=this.props.value
@ -2500,7 +2505,7 @@ return u["default"].createElement(f.Tab.Pane,e,this.props.children)}}]),t}(d["de
p.propTypes={name:u["default"].PropTypes.string.isRequired,extraClass:u["default"].PropTypes.string,tabClassName:u["default"].PropTypes.string},p.defaultProps={className:"",extraClass:""},t["default"]=p p.propTypes={name:u["default"].PropTypes.string.isRequired,extraClass:u["default"].PropTypes.string,tabClassName:u["default"].PropTypes.string},p.defaultProps={className:"",extraClass:""},t["default"]=p
},function(e,t){e.exports=FormAction},function(e,t,n){"use strict" },function(e,t){e.exports=FormAction},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(140),o=n(145),a=i(o),s=n(218),l=i(s),u=n(249),c=i(u) function i(e){return e&&e.__esModule?e:{"default":e}}var r=n(141),o=n(146),a=i(o),s=n(219),l=i(s),u=n(249),c=i(u)
document.addEventListener("DOMContentLoaded",function(){var e=a["default"].getSection("SilverStripe\\Admin\\CampaignAdmin") document.addEventListener("DOMContentLoaded",function(){var e=a["default"].getSection("SilverStripe\\Admin\\CampaignAdmin")
l["default"].add({path:e.url,component:(0,r.withRouter)(c["default"]),childRoutes:[{path:":type/:id/:view",component:c["default"]},{path:"set/:id/:view",component:c["default"]}]})})},function(e,t,n){"use strict" l["default"].add({path:e.url,component:(0,r.withRouter)(c["default"]),childRoutes:[{path:":type/:id/:view",component:c["default"]},{path:"set/:id/:view",component:c["default"]}]})})},function(e,t,n){"use strict"
@ -2517,7 +2522,7 @@ campaignId:e.campaign.campaignId,view:e.campaign.view,breadcrumbs:e.breadcrumbs,
breadcrumbsActions:(0,m.bindActionCreators)(_,e)}}Object.defineProperty(t,"__esModule",{value:!0}) breadcrumbsActions:(0,m.bindActionCreators)(_,e)}}Object.defineProperty(t,"__esModule",{value:!0})
var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t] var c=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]
for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},d=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e},d=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),f=n(5),p=r(f),h=n(107),m=n(108),g=n(140),v=n(103),y=r(v),b=n(250),_=i(b),w=n(251),C=r(w),T=n(21),E=r(T),P=n(247),O=r(P),S=n(114),k=r(S),j=n(252),x=r(j),R=n(115),I=r(R),A=n(253),D=r(A),F=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),f=n(5),p=r(f),h=n(107),m=n(108),g=n(141),v=n(103),y=r(v),b=n(250),_=i(b),w=n(251),C=r(w),T=n(21),E=r(T),P=n(247),O=r(P),S=n(115),k=r(S),j=n(252),x=r(j),R=n(116),I=r(R),A=n(253),D=r(A),F=function(e){
function t(e){o(this,t) function t(e){o(this,t)
var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.publishApi=y["default"].createEndpointFetcher({url:n.props.sectionConfig.publishEndpoint.url,method:n.props.sectionConfig.publishEndpoint.method,defaultData:{SecurityID:n.props.securityId},payloadSchema:{ return n.publishApi=y["default"].createEndpointFetcher({url:n.props.sectionConfig.publishEndpoint.url,method:n.props.sectionConfig.publishEndpoint.method,defaultData:{SecurityID:n.props.securityId},payloadSchema:{
@ -2591,7 +2596,7 @@ var i=Object.getOwnPropertyDescriptor(e,t)
if(void 0===i){var r=Object.getPrototypeOf(e) if(void 0===i){var r=Object.getPrototypeOf(e)
return null===r?void 0:q(r,t,n)}if("value"in i)return i.value return null===r?void 0:q(r,t,n)}if("value"in i)return i.value
var o=i.get var o=i.get
if(void 0!==o)return o.call(n)},p=n(5),h=r(p),m=n(108),g=n(107),v=n(250),y=i(v),b=n(124),_=i(b),w=n(254),C=i(w),T=n(21),E=r(T),P=n(255),O=r(P),S=n(256),k=r(S),j=n(258),x=r(j),R=n(252),I=r(R),A=n(247),D=r(A),F=n(259),M=r(F),N=n(251),U=r(N),L=n(260),H=r(L),B=n(114),$=r(B),V=function(e){ if(void 0!==o)return o.call(n)},p=n(5),h=r(p),m=n(108),g=n(107),v=n(250),y=i(v),b=n(125),_=i(b),w=n(254),C=i(w),T=n(21),E=r(T),P=n(255),O=r(P),S=n(256),k=r(S),j=n(258),x=r(j),R=n(252),I=r(R),A=n(247),D=r(A),F=n(259),M=r(F),N=n(251),U=r(N),L=n(260),H=r(L),B=n(115),$=r(B),V=function(e){
function t(e){o(this,t) function t(e){o(this,t)
var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=a(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handlePublish=n.handlePublish.bind(n),n.handleItemSelected=n.handleItemSelected.bind(n),n.setBreadcrumbs=n.setBreadcrumbs.bind(n),n.handleCloseItem=n.handleCloseItem.bind(n),n}return s(t,e),d(t,[{ return n.handlePublish=n.handlePublish.bind(n),n.handleItemSelected=n.handleItemSelected.bind(n),n.setBreadcrumbs=n.setBreadcrumbs.bind(n),n.handleCloseItem=n.handleCloseItem.bind(n),n}return s(t,e),d(t,[{
@ -2634,7 +2639,7 @@ function i(e){return e&&e.__esModule?e:{"default":e}}function r(e){return{type:l
payload:{campaignId:e,view:t}})}}function a(e,t,n){return function(i){i({type:l["default"].PUBLISH_CAMPAIGN_REQUEST,payload:{campaignId:n}}),e({id:n}).then(function(e){i({type:l["default"].PUBLISH_CAMPAIGN_SUCCESS, payload:{campaignId:e,view:t}})}}function a(e,t,n){return function(i){i({type:l["default"].PUBLISH_CAMPAIGN_REQUEST,payload:{campaignId:n}}),e({id:n}).then(function(e){i({type:l["default"].PUBLISH_CAMPAIGN_SUCCESS,
payload:{campaignId:n}}),i({type:c["default"].FETCH_RECORD_SUCCESS,payload:{recordType:t,data:e}})})["catch"](function(e){i({type:l["default"].PUBLISH_CAMPAIGN_FAILURE,payload:{error:e}})})}}Object.defineProperty(t,"__esModule",{ payload:{campaignId:n}}),i({type:c["default"].FETCH_RECORD_SUCCESS,payload:{recordType:t,data:e}})})["catch"](function(e){i({type:l["default"].PUBLISH_CAMPAIGN_FAILURE,payload:{error:e}})})}}Object.defineProperty(t,"__esModule",{
value:!0}),t.selectChangeSetItem=r,t.showCampaignView=o,t.publishCampaign=a value:!0}),t.selectChangeSetItem=r,t.showCampaignView=o,t.publishCampaign=a
var s=n(230),l=i(s),u=n(125),c=i(u)},function(e,t,n){"use strict" var s=n(230),l=i(s),u=n(126),c=i(u)},function(e,t,n){"use strict"
function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called") function i(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
@ -2684,7 +2689,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}) value:!0})
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(114),p=i(f),h=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(21),d=i(c),f=n(115),p=i(f),h=function(e){
function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"render",value:function n(){var e=null,t={},n=this.props.item,i=this.props.campaign function t(){return r(this,t),o(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"render",value:function n(){var e=null,t={},n=this.props.item,i=this.props.campaign
@ -2707,7 +2712,7 @@ return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("funct
e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{ e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{
value:!0}) value:!0})
var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n] var s=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n]
i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(114),d=i(c),f=n(21),p=i(f),h=function(e){ i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),l=n(5),u=i(l),c=n(115),d=i(c),f=n(21),p=i(f),h=function(e){
function t(e){r(this,t) function t(e){r(this,t)
var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e)) var n=o(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e))
return n.handleBackClick=n.handleBackClick.bind(n),n}return a(t,e),s(t,[{key:"handleBackClick",value:function n(e){"function"==typeof this.props.onBack&&(e.preventDefault(),this.props.onBack(e))}},{key:"render", return n.handleBackClick=n.handleBackClick.bind(n),n}return a(t,e),s(t,[{key:"handleBackClick",value:function n(e){"function"==typeof this.props.onBack&&(e.preventDefault(),this.props.onBack(e))}},{key:"render",

View File

@ -12,8 +12,8 @@ else{i[t]=[n]
var r=document.getElementsByTagName("head")[0],o=document.createElement("script") var r=document.getElementsByTagName("head")[0],o=document.createElement("script")
o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+t+".js/"+({0:"CMSSecurity",1:"LeftAndMain.Ping",2:"MemberImportForm",3:"TinyMCE_SSPlugin",4:"UploadField_select",5:"bundle",6:"leaktools" o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=e.p+""+t+".js/"+({0:"CMSSecurity",1:"LeftAndMain.Ping",2:"MemberImportForm",3:"TinyMCE_SSPlugin",4:"UploadField_select",5:"bundle",6:"leaktools"
}[t]||t)+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="",e(0)}([function(t,e,n){"use strict" }[t]||t)+".js",r.appendChild(o)}},e.m=t,e.c=r,e.p="",e(0)}([function(t,e,n){"use strict"
n(261),n(557),n(558),n(561),n(563),n(565),n(595),n(597),n(730),n(740),n(755),n(930),n(932),n(980),n(986),n(1240),n(1248),n(1251),n(1254),n(1257),n(1258),n(162),n(192),n(196),n(1259),n(1260),n(1261),n(1262), n(261),n(557),n(558),n(561),n(563),n(565),n(595),n(597),n(730),n(740),n(755),n(930),n(932),n(980),n(986),n(1240),n(1248),n(1251),n(1254),n(1257),n(1258),n(163),n(193),n(197),n(1259),n(1260),n(1261),n(1262),
n(193),n(1263),n(1264),n(1265),n(1266),n(1267),n(1268),n(1269),n(1270)},function(t,e){t.exports=jQuery},,,,function(t,e){t.exports=React},,,,,,function(t,e){function n(){throw new Error("setTimeout has not been defined") n(194),n(1263),n(1264),n(1265),n(1266),n(1267),n(1268),n(1269),n(1270)},function(t,e){t.exports=jQuery},,,,function(t,e){t.exports=React},,,,,,function(t,e){function n(){throw new Error("setTimeout has not been defined")
}function r(){throw new Error("clearTimeout has not been defined")}function i(t){if(d===setTimeout)return setTimeout(t,0) }function r(){throw new Error("clearTimeout has not been defined")}function i(t){if(d===setTimeout)return setTimeout(t,0)
if((d===n||!d)&&setTimeout)return d=setTimeout,setTimeout(t,0) if((d===n||!d)&&setTimeout)return d=setTimeout,setTimeout(t,0)
@ -402,7 +402,7 @@ var o=n(48),a=r(o),s=n(49),l=r(s),u={all_lowercase:!0,gmail_lowercase:!0,gmail_r
yahoo_lowercase:!0,yahoo_remove_subaddress:!0,icloud_lowercase:!0,icloud_remove_subaddress:!0},c=["icloud.com","me.com"],d=["hotmail.at","hotmail.be","hotmail.ca","hotmail.cl","hotmail.co.il","hotmail.co.nz","hotmail.co.th","hotmail.co.uk","hotmail.com","hotmail.com.ar","hotmail.com.au","hotmail.com.br","hotmail.com.gr","hotmail.com.mx","hotmail.com.pe","hotmail.com.tr","hotmail.com.vn","hotmail.cz","hotmail.de","hotmail.dk","hotmail.es","hotmail.fr","hotmail.hu","hotmail.id","hotmail.ie","hotmail.in","hotmail.it","hotmail.jp","hotmail.kr","hotmail.lv","hotmail.my","hotmail.ph","hotmail.pt","hotmail.sa","hotmail.sg","hotmail.sk","live.be","live.co.uk","live.com","live.com.ar","live.com.mx","live.de","live.es","live.eu","live.fr","live.it","live.nl","msn.com","outlook.at","outlook.be","outlook.cl","outlook.co.il","outlook.co.nz","outlook.co.th","outlook.com","outlook.com.ar","outlook.com.au","outlook.com.br","outlook.com.gr","outlook.com.pe","outlook.com.tr","outlook.com.vn","outlook.cz","outlook.de","outlook.dk","outlook.es","outlook.fr","outlook.hu","outlook.id","outlook.ie","outlook.in","outlook.it","outlook.jp","outlook.kr","outlook.lv","outlook.my","outlook.ph","outlook.pt","outlook.sa","outlook.sg","outlook.sk","passport.com"],f=["rocketmail.com","yahoo.ca","yahoo.co.uk","yahoo.com","yahoo.de","yahoo.fr","yahoo.in","yahoo.it","ymail.com"] yahoo_lowercase:!0,yahoo_remove_subaddress:!0,icloud_lowercase:!0,icloud_remove_subaddress:!0},c=["icloud.com","me.com"],d=["hotmail.at","hotmail.be","hotmail.ca","hotmail.cl","hotmail.co.il","hotmail.co.nz","hotmail.co.th","hotmail.co.uk","hotmail.com","hotmail.com.ar","hotmail.com.au","hotmail.com.br","hotmail.com.gr","hotmail.com.mx","hotmail.com.pe","hotmail.com.tr","hotmail.com.vn","hotmail.cz","hotmail.de","hotmail.dk","hotmail.es","hotmail.fr","hotmail.hu","hotmail.id","hotmail.ie","hotmail.in","hotmail.it","hotmail.jp","hotmail.kr","hotmail.lv","hotmail.my","hotmail.ph","hotmail.pt","hotmail.sa","hotmail.sg","hotmail.sk","live.be","live.co.uk","live.com","live.com.ar","live.com.mx","live.de","live.es","live.eu","live.fr","live.it","live.nl","msn.com","outlook.at","outlook.be","outlook.cl","outlook.co.il","outlook.co.nz","outlook.co.th","outlook.com","outlook.com.ar","outlook.com.au","outlook.com.br","outlook.com.gr","outlook.com.pe","outlook.com.tr","outlook.com.vn","outlook.cz","outlook.de","outlook.dk","outlook.es","outlook.fr","outlook.hu","outlook.id","outlook.ie","outlook.in","outlook.it","outlook.jp","outlook.kr","outlook.lv","outlook.my","outlook.ph","outlook.pt","outlook.sa","outlook.sg","outlook.sk","passport.com"],f=["rocketmail.com","yahoo.ca","yahoo.co.uk","yahoo.com","yahoo.de","yahoo.fr","yahoo.in","yahoo.it","ymail.com"]
t.exports=e["default"]},,,,,function(t,e){t.exports=ReactRedux},function(t,e){t.exports=Redux},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(module,exports,__webpack_require__){(function(jQuery){ t.exports=e["default"]},,,,,function(t,e){t.exports=ReactRedux},function(t,e){t.exports=Redux},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(module,exports,__webpack_require__){(function(jQuery){
!function(t,e){function n(e,n){var i,o,a,s=e.nodeName.toLowerCase() !function(t,e){function n(e,n){var i,o,a,s=e.nodeName.toLowerCase()
return"area"===s?(i=e.parentNode,o=i.name,!(!e.href||!o||"map"!==i.nodeName.toLowerCase())&&(a=t("img[usemap=#"+o+"]")[0],!!a&&r(a))):(/input|select|textarea|button|object/.test(s)?!e.disabled:"a"===s?e.href||n:n)&&r(e) return"area"===s?(i=e.parentNode,o=i.name,!(!e.href||!o||"map"!==i.nodeName.toLowerCase())&&(a=t("img[usemap=#"+o+"]")[0],!!a&&r(a))):(/input|select|textarea|button|object/.test(s)?!e.disabled:"a"===s?e.href||n:n)&&r(e)
@ -3362,7 +3362,7 @@ c.canUseDOM?void 0:s["default"](!1)
var v=m.forceRefresh,g=d.supportsHistory(),y=!g||v,_=h["default"](o({},m,{getCurrentLocation:t,finishTransition:n,saveState:f.saveState})),b=0,x=void 0 var v=m.forceRefresh,g=d.supportsHistory(),y=!g||v,_=h["default"](o({},m,{getCurrentLocation:t,finishTransition:n,saveState:f.saveState})),b=0,x=void 0
return o({},_,{listenBefore:r,listen:i,registerTransitionHook:a,unregisterTransitionHook:p})}e.__esModule=!0 return o({},_,{listenBefore:r,listen:i,registerTransitionHook:a,unregisterTransitionHook:p})}e.__esModule=!0
var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(201),s=r(a),l=n(202),u=n(203),c=n(205),d=n(206),f=n(207),p=n(208),h=r(p) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(202),s=r(a),l=n(203),u=n(204),c=n(206),d=n(207),f=n(208),p=n(209),h=r(p)
e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict"
var r=function(t,e,n,r,i,o,a,s){if(!t){var l var r=function(t,e,n,r,i,o,a,s){if(!t){var l
if(void 0===e)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.") if(void 0===e)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.")
@ -3381,7 +3381,7 @@ return null==e?t:t.substring(e[0].length)}function o(t){var e=i(t),n="",r="",o=e
o!==-1&&(r=e.substring(o),e=e.substring(0,o)) o!==-1&&(r=e.substring(o),e=e.substring(0,o))
var a=e.indexOf("?") var a=e.indexOf("?")
return a!==-1&&(n=e.substring(a),e=e.substring(0,a)),""===e&&(e="/"),{pathname:e,search:n,hash:r}}e.__esModule=!0,e.extractPath=i,e.parsePath=o return a!==-1&&(n=e.substring(a),e=e.substring(0,a)),""===e&&(e="/"),{pathname:e,search:n,hash:r}}e.__esModule=!0,e.extractPath=i,e.parsePath=o
var a=n(204),s=r(a)},function(t,e,n){"use strict" var a=n(205),s=r(a)},function(t,e,n){"use strict"
var r=function(){} var r=function(){}
t.exports=r},function(t,e){"use strict" t.exports=r},function(t,e){"use strict"
e.__esModule=!0 e.__esModule=!0
@ -3403,12 +3403,12 @@ if(n.name===d)return
if(c.indexOf(n.name)>=0&&0===window.sessionStorage.length)return if(c.indexOf(n.name)>=0&&0===window.sessionStorage.length)return
throw n}}function a(t){var e=void 0 throw n}}function a(t){var e=void 0
try{e=window.sessionStorage.getItem(i(t))}catch(n){if(n.name===d)return null}if(e)try{return JSON.parse(e)}catch(n){}return null}e.__esModule=!0,e.saveState=o,e.readState=a try{e=window.sessionStorage.getItem(i(t))}catch(n){if(n.name===d)return null}if(e)try{return JSON.parse(e)}catch(n){}return null}e.__esModule=!0,e.saveState=o,e.readState=a
var s=n(204),l=r(s),u="@@History/",c=["QuotaExceededError","QUOTA_EXCEEDED_ERR"],d="SecurityError"},function(t,e,n){"use strict" var s=n(205),l=r(s),u="@@History/",c=["QuotaExceededError","QUOTA_EXCEEDED_ERR"],d="SecurityError"},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){function e(t){return l.canUseDOM?void 0:s["default"](!1),n.listen(t)}var n=d["default"](o({getUserConfirmation:u.getUserConfirmation},t,{ function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){function e(t){return l.canUseDOM?void 0:s["default"](!1),n.listen(t)}var n=d["default"](o({getUserConfirmation:u.getUserConfirmation},t,{
go:u.go})) go:u.go}))
return o({},n,{listen:e})}e.__esModule=!0 return o({},n,{listen:e})}e.__esModule=!0
var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(201),s=r(a),l=n(205),u=n(206),c=n(209),d=r(c) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(202),s=r(a),l=n(206),u=n(207),c=n(210),d=r(c)
e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return Math.random().toString(36).substr(2,t)}function o(t,e){return t.pathname===e.pathname&&t.search===e.search&&t.key===e.key&&d["default"](t.state,e.state) function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return Math.random().toString(36).substr(2,t)}function o(t,e){return t.pathname===e.pathname&&t.search===e.search&&t.key===e.key&&d["default"](t.state,e.state)
@ -3434,7 +3434,7 @@ return{listenBefore:t,listen:r,transitionTo:l,push:u,replace:c,go:I,goBack:m,goF
registerTransitionHook:b["default"](E,"registerTransitionHook is deprecated; use listenBefore instead"),unregisterTransitionHook:b["default"](S,"unregisterTransitionHook is deprecated; use the callback returned from listenBefore instead"), registerTransitionHook:b["default"](E,"registerTransitionHook is deprecated; use listenBefore instead"),unregisterTransitionHook:b["default"](S,"unregisterTransitionHook is deprecated; use the callback returned from listenBefore instead"),
pushState:b["default"](P,"pushState is deprecated; use push instead"),replaceState:b["default"](O,"replaceState is deprecated; use replace instead")}}e.__esModule=!0 pushState:b["default"](P,"pushState is deprecated; use push instead"),replaceState:b["default"](O,"replaceState is deprecated; use replace instead")}}e.__esModule=!0
var s=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var s=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},l=n(204),u=r(l),c=n(210),d=r(c),f=n(203),p=n(213),h=n(202),m=n(214),v=r(m),g=n(215),y=r(g),_=n(216),b=r(_),x=6 for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},l=n(205),u=r(l),c=n(211),d=r(c),f=n(204),p=n(214),h=n(203),m=n(215),v=r(m),g=n(216),y=r(g),_=n(217),b=r(_),x=6
e["default"]=a,t.exports=e["default"]},function(t,e,n){function r(t){return null===t||void 0===t}function i(t){return!(!t||"object"!=typeof t||"number"!=typeof t.length)&&("function"==typeof t.copy&&"function"==typeof t.slice&&!(t.length>0&&"number"!=typeof t[0])) e["default"]=a,t.exports=e["default"]},function(t,e,n){function r(t){return null===t||void 0===t}function i(t){return!(!t||"object"!=typeof t||"number"!=typeof t.length)&&("function"==typeof t.copy&&"function"==typeof t.slice&&!(t.length>0&&"number"!=typeof t[0]))
}function o(t,e,n){var o,c }function o(t,e,n){var o,c
@ -3447,7 +3447,7 @@ for(o=0;o<t.length;o++)if(t[o]!==e[o])return!1
return!0}try{var d=s(t),f=s(e)}catch(p){return!1}if(d.length!=f.length)return!1 return!0}try{var d=s(t),f=s(e)}catch(p){return!1}if(d.length!=f.length)return!1
for(d.sort(),f.sort(),o=d.length-1;o>=0;o--)if(d[o]!=f[o])return!1 for(d.sort(),f.sort(),o=d.length-1;o>=0;o--)if(d[o]!=f[o])return!1
for(o=d.length-1;o>=0;o--)if(c=d[o],!u(t[c],e[c],n))return!1 for(o=d.length-1;o>=0;o--)if(c=d[o],!u(t[c],e[c],n))return!1
return typeof t==typeof e}var a=Array.prototype.slice,s=n(211),l=n(212),u=t.exports=function(t,e,n){return n||(n={}),t===e||(t instanceof Date&&e instanceof Date?t.getTime()===e.getTime():!t||!e||"object"!=typeof t&&"object"!=typeof e?n.strict?t===e:t==e:o(t,e,n)) return typeof t==typeof e}var a=Array.prototype.slice,s=n(212),l=n(213),u=t.exports=function(t,e,n){return n||(n={}),t===e||(t instanceof Date&&e instanceof Date?t.getTime()===e.getTime():!t||!e||"object"!=typeof t&&"object"!=typeof e?n.strict?t===e:t==e:o(t,e,n))
}},function(t,e){function n(t){var e=[] }},function(t,e){function n(t){var e=[]
for(var n in t)e.push(n) for(var n in t)e.push(n)
@ -3467,15 +3467,15 @@ function r(t){return t&&t.__esModule?t:{"default":t}}function i(){var t=argument
var i=t.pathname||"/",a=t.search||"",s=t.hash||"",c=t.state||null var i=t.pathname||"/",a=t.search||"",s=t.hash||"",c=t.state||null
return{pathname:i,search:a,hash:s,state:c,action:e,key:n}}e.__esModule=!0 return{pathname:i,search:a,hash:s,state:c,action:e,key:n}}e.__esModule=!0
var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(204),s=r(a),l=n(202),u=n(203) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(205),s=r(a),l=n(203),u=n(204)
e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e,n){var r=t(e,n) function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e,n){var r=t(e,n)
t.length<2&&n(r)}e.__esModule=!0 t.length<2&&n(r)}e.__esModule=!0
var o=n(204),a=r(o) var o=n(205),a=r(o)
e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return function(){return t.apply(this,arguments)}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return function(){return t.apply(this,arguments)}}e.__esModule=!0
var o=n(204),a=r(o) var o=n(205),a=r(o)
e["default"]=i,t.exports=e["default"]},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(t,e,n){(function(t){"use strict" e["default"]=i,t.exports=e["default"]},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(t,e,n){(function(t){"use strict"
function e(t,e,n){t[e]||Object[r](t,e,{writable:!0,configurable:!0,value:n})}if(n(262),n(553),n(554),t._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed") function e(t,e,n){t[e]||Object[r](t,e,{writable:!0,configurable:!0,value:n})}if(n(262),n(553),n(554),t._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed")
t._babelPolyfill=!0 t._babelPolyfill=!0
var r="defineProperty" var r="defineProperty"
@ -7502,7 +7502,7 @@ var E=T.queryKey;(void 0===E||E)&&(E="string"==typeof E?E:w)
var S=x["default"](c({},T,{getCurrentLocation:t,finishTransition:n,saveState:_.saveState})),P=0,O=void 0,M=y.supportsGoWithoutReloadUsingHash() var S=x["default"](c({},T,{getCurrentLocation:t,finishTransition:n,saveState:_.saveState})),P=0,O=void 0,M=y.supportsGoWithoutReloadUsingHash()
return c({},S,{listenBefore:r,listen:i,push:u,replace:d,go:f,createHref:p,registerTransitionHook:b,unregisterTransitionHook:k,pushState:C,replaceState:j})}e.__esModule=!0 return c({},S,{listenBefore:r,listen:i,push:u,replace:d,go:f,createHref:p,registerTransitionHook:b,unregisterTransitionHook:k,pushState:C,replaceState:j})}e.__esModule=!0
var c=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var c=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},d=n(204),f=r(d),p=n(201),h=r(p),m=n(202),v=n(203),g=n(205),y=n(206),_=n(207),b=n(208),x=r(b),w="_k" for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},d=n(205),f=r(d),p=n(202),h=r(p),m=n(203),v=n(204),g=n(206),y=n(207),_=n(208),b=n(209),x=r(b),w="_k"
e["default"]=u,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=u,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return c.stringify(t).replace(/%20/g,"+")}function o(t){for(var e in t)if(Object.prototype.hasOwnProperty.call(t,e)&&"object"==typeof t[e]&&!Array.isArray(t[e])&&null!==t[e])return!0 function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return c.stringify(t).replace(/%20/g,"+")}function o(t){for(var e in t)if(Object.prototype.hasOwnProperty.call(t,e)&&"object"==typeof t[e]&&!Array.isArray(t[e])&&null!==t[e])return!0
@ -7525,7 +7525,7 @@ return t.query&&(a.query=t.query),e(a)}function h(t,e,n){"string"==typeof e&&(e=
return"function"!=typeof x&&(x=i),"function"!=typeof w&&(w=g),s({},b,{listenBefore:r,listen:o,push:a,replace:l,createPath:u,createHref:c,createLocation:d,pushState:m["default"](h,"pushState is deprecated; use push instead"), return"function"!=typeof x&&(x=i),"function"!=typeof w&&(w=g),s({},b,{listenBefore:r,listen:o,push:a,replace:l,createPath:u,createHref:c,createLocation:d,pushState:m["default"](h,"pushState is deprecated; use push instead"),
replaceState:m["default"](y,"replaceState is deprecated; use replace instead")})}}e.__esModule=!0 replaceState:m["default"](y,"replaceState is deprecated; use replace instead")})}}e.__esModule=!0
var s=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var s=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},l=n(204),u=r(l),c=n(945),d=n(215),f=r(d),p=n(203),h=n(216),m=r(h),v="$searchBase",g=c.parse for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},l=n(205),u=r(l),c=n(945),d=n(216),f=r(d),p=n(204),h=n(217),m=r(h),v="$searchBase",g=c.parse
e["default"]=a,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=a,t.exports=e["default"]},function(t,e,n){"use strict"
var r=n(946) var r=n(946)
e.extract=function(t){return t.split("?")[1]||""},e.parse=function(t){return"string"!=typeof t?{}:(t=t.trim().replace(/^(\?|#|&)/,""),t?t.split("&").reduce(function(t,e){var n=e.replace(/\+/g," ").split("="),r=n.shift(),i=n.length>0?n.join("="):void 0 e.extract=function(t){return t.split("?")[1]||""},e.parse=function(t){return"string"!=typeof t?{}:(t=t.trim().replace(/^(\?|#|&)/,""),t?t.split("&").reduce(function(t,e){var n=e.replace(/\+/g," ").split("="),r=n.shift(),i=n.length>0?n.join("="):void 0
@ -7782,7 +7782,7 @@ n=(0,m.createRoutingHistory)(n,c),c.match(o,function(t,r,i){e(t,r&&v.createLocat
var a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},s=n(202),l=n(941),u=r(l),c=n(972),d=r(c),f=n(947),p=r(f),h=n(934),m=n(957) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},s=n(203),l=n(941),u=r(l),c=n(972),d=r(c),f=n(947),p=r(f),h=n(934),m=n(957)
e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=(0,c["default"])(t),n=function i(){return e},r=(0,a["default"])((0,l["default"])(n))(t) function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=(0,c["default"])(t),n=function i(){return e},r=(0,a["default"])((0,l["default"])(n))(t)
return r.__v2_compatible__=!0,r}e.__esModule=!0,e["default"]=i return r.__v2_compatible__=!0,r}e.__esModule=!0,e["default"]=i
@ -7805,7 +7805,7 @@ return n(_.createLocation.apply(_,[r(t)].concat(i)))}function v(t,e){"string"==t
return o({},_,{listenBefore:i,listen:a,push:s,replace:c,createPath:f,createHref:h,createLocation:m,pushState:p["default"](v,"pushState is deprecated; use push instead"),replaceState:p["default"](g,"replaceState is deprecated; use replace instead") return o({},_,{listenBefore:i,listen:a,push:s,replace:c,createPath:f,createHref:h,createLocation:m,pushState:p["default"](v,"pushState is deprecated; use push instead"),replaceState:p["default"](g,"replaceState is deprecated; use replace instead")
})}}e.__esModule=!0 })}}e.__esModule=!0
var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(204),s=r(a),l=n(205),u=n(203),c=n(215),d=r(c),f=n(216),p=r(f) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},a=n(205),s=r(a),l=n(206),u=n(204),c=n(216),d=r(c),f=n(217),p=r(f)
e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=i,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return t.filter(function(t){return t.state}).reduce(function(t,e){return t[e.key]=e.state,t},{})}function o(){function t(t,e){g[t]=e}function e(t){ function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return t.filter(function(t){return t.state}).reduce(function(t,e){return t[e.key]=e.state,t},{})}function o(){function t(t,e){g[t]=e}function e(t){
return g[t]}function n(){var t=m[v],n=t.basename,r=t.pathname,i=t.search,o=(n||"")+r+(i||""),s=void 0,l=void 0 return g[t]}function n(){var t=m[v],n=t.basename,r=t.pathname,i=t.search,o=(n||"")+r+(i||""),s=void 0,l=void 0
@ -7825,7 +7825,7 @@ return"string"==typeof t?{pathname:t,key:e}:"object"==typeof t&&t?a({},t,{key:e}
var g=i(m) var g=i(m)
return u}e.__esModule=!0 return u}e.__esModule=!0
var a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},s=n(204),l=r(s),u=n(201),c=r(u),d=n(203),f=n(202),p=n(209),h=r(p) for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},s=n(205),l=r(s),u=n(202),c=r(u),d=n(204),f=n(203),p=n(210),h=r(p)
e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=o,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return function(e){var n=(0,a["default"])((0,l["default"])(t))(e) function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return function(e){var n=(0,a["default"])((0,l["default"])(t))(e)
return n.__v2_compatible__=!0,n}}e.__esModule=!0,e["default"]=i return n.__v2_compatible__=!0,n}}e.__esModule=!0,e["default"]=i
@ -7843,7 +7843,7 @@ return function(t){return r.reduceRight(function(e,n){return n(e,t)},a["default"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(200),o=r(i),a=n(978),s=r(a) var i=n(201),o=r(i),a=n(978),s=r(a)
e["default"]=(0,s["default"])(o["default"]),t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=(0,s["default"])(o["default"]),t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0,e["default"]=function(t){var e=void 0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0,e["default"]=function(t){var e=void 0
return a&&(e=(0,o["default"])(t)()),e} return a&&(e=(0,o["default"])(t)()),e}
@ -8325,7 +8325,7 @@ s["default"])(t,["componentClass","className"]),i=(0,b.splitBsProps)(r),a=i[0],l
return g["default"].createElement(e,(0,o["default"])({},l,{className:(0,m["default"])(n,u)}))},e}(g["default"].Component) return g["default"].createElement(e,(0,o["default"])({},l,{className:(0,m["default"])(n,u)}))},e}(g["default"].Component)
k.propTypes=x,k.defaultProps=w,e["default"]=(0,b.bsClass)("carousel-caption",k),t.exports=e["default"]},function(t,e,n){"use strict" k.propTypes=x,k.defaultProps=w,e["default"]=(0,b.bsClass)("carousel-caption",k),t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(5),g=r(v),y=n(176),_=r(y),b=n(1101),x=r(b),w={direction:g["default"].PropTypes.oneOf(["prev","next"]), var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(5),g=r(v),y=n(177),_=r(y),b=n(1101),x=r(b),w={direction:g["default"].PropTypes.oneOf(["prev","next"]),
onAnimateOutEnd:g["default"].PropTypes.func,active:g["default"].PropTypes.bool,animateIn:g["default"].PropTypes.bool,animateOut:g["default"].PropTypes.bool,index:g["default"].PropTypes.number},k={active:!1, onAnimateOutEnd:g["default"].PropTypes.func,active:g["default"].PropTypes.bool,animateIn:g["default"].PropTypes.bool,animateOut:g["default"].PropTypes.bool,index:g["default"].PropTypes.number},k={active:!1,
animateIn:!1,animateOut:!1},C=function(t){function e(n,r){(0,u["default"])(this,e) animateIn:!1,animateOut:!1},C=function(t){function e(n,r){(0,u["default"])(this,e)
var i=(0,d["default"])(this,t.call(this,n,r)) var i=(0,d["default"])(this,t.call(this,n,r))
@ -8458,7 +8458,7 @@ t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,wri
value:!0}),e.EXITING=e.ENTERED=e.ENTERING=e.EXITED=e.UNMOUNTED=void 0 value:!0}),e.EXITING=e.ENTERED=e.ENTERING=e.EXITED=e.UNMOUNTED=void 0
var u=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var u=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},c=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n] for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},c=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n]
r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),d=n(5),f=r(d),p=n(176),h=r(p),m=n(1119),v=r(m),g=n(1121),y=r(g),_=n(1074),b=r(_),x=v["default"].end,w=e.UNMOUNTED=0,k=e.EXITED=1,C=e.ENTERING=2,j=e.ENTERED=3,T=e.EXITING=4,E=function(t){ r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),d=n(5),f=r(d),p=n(177),h=r(p),m=n(1119),v=r(m),g=n(1121),y=r(g),_=n(1074),b=r(_),x=v["default"].end,w=e.UNMOUNTED=0,k=e.EXITED=1,C=e.ENTERING=2,j=e.ENTERED=3,T=e.EXITING=4,E=function(t){
function e(t,n){o(this,e) function e(t,n){o(this,e)
var r=a(this,Object.getPrototypeOf(e).call(this,t,n)),i=void 0 var r=a(this,Object.getPrototypeOf(e).call(this,t,n)),i=void 0
return i=t["in"]?t.transitionAppear?k:j:t.unmountOnExit?w:k,r.state={status:i},r.nextCallback=null,r}return s(e,t),c(e,[{key:"componentDidMount",value:function n(){this.props.transitionAppear&&this.props["in"]&&this.performEnter(this.props) return i=t["in"]?t.transitionAppear?k:j:t.unmountOnExit?w:k,r.state={status:i},r.nextCallback=null,r}return s(e,t),c(e,[{key:"componentDidMount",value:function n(){this.props.transitionAppear&&this.props["in"]&&this.performEnter(this.props)
@ -8500,7 +8500,7 @@ var r=n(1120),i=function o(){}
r&&(i=function(){return document.addEventListener?function(t,e,n,r){return t.addEventListener(e,n,r||!1)}:document.attachEvent?function(t,e,n){return t.attachEvent("on"+e,n)}:void 0}()),t.exports=i},function(t,e,n){ r&&(i=function(){return document.addEventListener?function(t,e,n,r){return t.addEventListener(e,n,r||!1)}:document.attachEvent?function(t,e,n){return t.attachEvent("on"+e,n)}:void 0}()),t.exports=i},function(t,e,n){
"use strict" "use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(1073),o=r(i),a=n(989),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(1123),g=r(v),y=n(1125),_=r(y),b=n(1126),x=r(b),w=n(5),k=r(w),C=n(176),j=r(C),T=n(1096),E=r(T),S=n(1092),P=r(S),O=n(1127),M=r(O),N=n(1128),A=r(N),D=n(1104),I=r(D),R=n(1095),F=r(R),L=n(1131),H=r(L),Q=n(1146),z=r(Q),W=n(1075),B=n(1082),U=r(B),q=n(1147),$=n(1083),V=r($),K=z["default"].defaultProps.bsRole,X=H["default"].defaultProps.bsRole,Y={ var i=n(1073),o=r(i),a=n(989),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(1123),g=r(v),y=n(1125),_=r(y),b=n(1126),x=r(b),w=n(5),k=r(w),C=n(177),j=r(C),T=n(1096),E=r(T),S=n(1092),P=r(S),O=n(1127),M=r(O),N=n(1128),A=r(N),D=n(1104),I=r(D),R=n(1095),F=r(R),L=n(1131),H=r(L),Q=n(1146),z=r(Q),W=n(1075),B=n(1082),U=r(B),q=n(1147),$=n(1083),V=r($),K=z["default"].defaultProps.bsRole,X=H["default"].defaultProps.bsRole,Y={
dropup:k["default"].PropTypes.bool,id:(0,M["default"])(k["default"].PropTypes.oneOfType([k["default"].PropTypes.string,k["default"].PropTypes.number])),componentClass:P["default"],children:(0,E["default"])((0, dropup:k["default"].PropTypes.bool,id:(0,M["default"])(k["default"].PropTypes.oneOfType([k["default"].PropTypes.string,k["default"].PropTypes.number])),componentClass:P["default"],children:(0,E["default"])((0,
q.requiredRoles)(K,X),(0,q.exclusiveRoles)(X)),disabled:k["default"].PropTypes.bool,pullRight:k["default"].PropTypes.bool,open:k["default"].PropTypes.bool,onClose:k["default"].PropTypes.func,onToggle:k["default"].PropTypes.func, q.requiredRoles)(K,X),(0,q.exclusiveRoles)(X)),disabled:k["default"].PropTypes.bool,pullRight:k["default"].PropTypes.bool,open:k["default"].PropTypes.bool,onClose:k["default"].PropTypes.func,onToggle:k["default"].PropTypes.func,
onSelect:k["default"].PropTypes.func,role:k["default"].PropTypes.string},G={componentClass:F["default"]},Z=function(t){function e(n,r){(0,u["default"])(this,e) onSelect:k["default"].PropTypes.func,role:k["default"].PropTypes.string},G={componentClass:F["default"]},Z=function(t){function e(n,r){(0,u["default"])(this,e)
@ -8609,7 +8609,7 @@ for(var r in t)h(t,r)&&e.call(n,t[r],r,t)}function h(t,e){return!!t&&Object.prot
e.uncontrolledPropTypes=o,e.getType=a,e.getValue=s,e.getLinkName=u,e.defaultKey=c,e.chain=d,e.transform=f,e.each=p,e.has=h,e.isReactComponent=m e.uncontrolledPropTypes=o,e.getType=a,e.getValue=s,e.getLinkName=u,e.defaultKey=c,e.chain=d,e.transform=f,e.each=p,e.has=h,e.isReactComponent=m
var v=n(5),g=r(v),y=n(1080),_=r(y),b=e.version=g["default"].version.split(".").map(parseFloat)},function(t,e,n){"use strict" var v=n(5),g=r(v),y=n(1080),_=r(y),b=e.version=g["default"].version.split(".").map(parseFloat)},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1132),u=r(l),c=n(1027),d=r(c),f=n(1028),p=r(f),h=n(1064),m=r(h),v=n(1074),g=r(v),y=n(1126),_=r(y),b=n(5),x=r(b),w=n(176),k=r(w),C=n(1141),j=r(C),T=n(1075),E=n(1082),S=r(E),P=n(1083),O=r(P),M={ var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1132),u=r(l),c=n(1027),d=r(c),f=n(1028),p=r(f),h=n(1064),m=r(h),v=n(1074),g=r(v),y=n(1126),_=r(y),b=n(5),x=r(b),w=n(177),k=r(w),C=n(1141),j=r(C),T=n(1075),E=n(1082),S=r(E),P=n(1083),O=r(P),M={
open:x["default"].PropTypes.bool,pullRight:x["default"].PropTypes.bool,onClose:x["default"].PropTypes.func,labelledBy:x["default"].PropTypes.oneOfType([x["default"].PropTypes.string,x["default"].PropTypes.number]), open:x["default"].PropTypes.bool,pullRight:x["default"].PropTypes.bool,onClose:x["default"].PropTypes.func,labelledBy:x["default"].PropTypes.oneOfType([x["default"].PropTypes.string,x["default"].PropTypes.number]),
onSelect:x["default"].PropTypes.func},N={bsRole:"menu",pullRight:!1},A=function(t){function e(n){(0,d["default"])(this,e) onSelect:x["default"].PropTypes.func},N={bsRole:"menu",pullRight:!1},A=function(t){function e(n){(0,d["default"])(this,e)
var r=(0,p["default"])(this,t.call(this,n)) var r=(0,p["default"])(this,t.call(this,n))
@ -8662,7 +8662,7 @@ t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,wri
}function u(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function c(){var t=w+"_"+k++ }function u(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function c(){var t=w+"_"+k++
return{id:t,suppressRootClose:function e(n){n.nativeEvent[t]=!0}}}Object.defineProperty(e,"__esModule",{value:!0}) return{id:t,suppressRootClose:function e(n){n.nativeEvent[t]=!0}}}Object.defineProperty(e,"__esModule",{value:!0})
var d=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n] var d=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n]
r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),f=n(5),p=r(f),h=n(176),m=r(h),v=n(1142),g=r(v),y=n(1144),_=r(y),b=n(1145),x=r(b),w="__click_was_inside",k=0,C=function(t){ r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),f=n(5),p=r(f),h=n(177),m=r(h),v=n(1142),g=r(v),y=n(1144),_=r(y),b=n(1145),x=r(b),w="__click_was_inside",k=0,C=function(t){
function e(t){o(this,e) function e(t){o(this,e)
var n=a(this,Object.getPrototypeOf(e).call(this,t)) var n=a(this,Object.getPrototypeOf(e).call(this,t))
n.handleDocumentMouse=n.handleDocumentMouse.bind(n),n.handleDocumentKeyUp=n.handleDocumentKeyUp.bind(n) n.handleDocumentMouse=n.handleDocumentMouse.bind(n),n.handleDocumentKeyUp=n.handleDocumentKeyUp.bind(n)
@ -8693,7 +8693,7 @@ return e.filter(function(t){return null!=t}).reduce(function(t,e){if("function"!
return null===t?e:function n(){for(var n=arguments.length,r=Array(n),i=0;i<n;i++)r[i]=arguments[i] return null===t?e:function n(){for(var n=arguments.length,r=Array(n),i=0;i<n;i++)r[i]=arguments[i]
t.apply(this,r),e.apply(this,r)}},null)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict" t.apply(this,r),e.apply(this,r)}},null)}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=function(t){return(0,s["default"])(o["default"].findDOMNode(t))} function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=function(t){return(0,s["default"])(o["default"].findDOMNode(t))}
var i=n(176),o=r(i),a=n(1124),s=r(a) var i=n(177),o=r(i),a=n(1124),s=r(a)
t.exports=e["default"]},function(t,e,n){"use strict" t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(5),m=r(h),v=n(1074),g=r(v),y=n(1094),_=r(y),b=n(1091),x=r(b),w=n(1075),k={noCaret:m["default"].PropTypes.bool, var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(5),m=r(h),v=n(1074),g=r(v),y=n(1094),_=r(y),b=n(1091),x=r(b),w=n(1075),k={noCaret:m["default"].PropTypes.bool,
@ -8907,7 +8907,7 @@ role:"heading",className:(0,m["default"])(l,(0,w.prefix)(f,"header")),style:u}))
o["default"])({},p,{role:"menuitem",tabIndex:"-1",onClick:(0,C["default"])(a,this.handleClick)})))},e}(g["default"].Component) o["default"])({},p,{role:"menuitem",tabIndex:"-1",onClick:(0,C["default"])(a,this.handleClick)})))},e}(g["default"].Component)
E.propTypes=j,E.defaultProps=T,e["default"]=(0,w.bsClass)("dropdown",E),t.exports=e["default"]},function(t,e,n){"use strict" E.propTypes=j,E.defaultProps=T,e["default"]=(0,w.bsClass)("dropdown",E),t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(1073),o=r(i),a=n(1027),s=r(a),l=n(1028),u=r(l),c=n(1064),d=r(c),f=n(989),p=r(f),h=n(1074),m=r(h),v=n(1175),g=r(v),y=n(1124),_=r(y),b=n(1120),x=r(b),w=n(1178),k=r(w),C=n(5),j=r(C),T=n(176),E=r(T),S=n(1179),P=r(S),O=n(1188),M=r(O),N=n(1092),A=r(N),D=n(1150),I=r(D),R=n(1192),F=r(R),L=n(1193),H=r(L),Q=n(1194),z=r(Q),W=n(1195),B=r(W),U=n(1196),q=r(U),$=n(1075),V=n(1082),K=r(V),X=n(1149),Y=r(X),G=n(1081),Z=(0, var i=n(1073),o=r(i),a=n(1027),s=r(a),l=n(1028),u=r(l),c=n(1064),d=r(c),f=n(989),p=r(f),h=n(1074),m=r(h),v=n(1175),g=r(v),y=n(1124),_=r(y),b=n(1120),x=r(b),w=n(1178),k=r(w),C=n(5),j=r(C),T=n(177),E=r(T),S=n(1179),P=r(S),O=n(1188),M=r(O),N=n(1092),A=r(N),D=n(1150),I=r(D),R=n(1192),F=r(R),L=n(1193),H=r(L),Q=n(1194),z=r(Q),W=n(1195),B=r(W),U=n(1196),q=r(U),$=n(1075),V=n(1082),K=r(V),X=n(1149),Y=r(X),G=n(1081),Z=(0,
p["default"])({},P["default"].propTypes,H["default"].propTypes,{backdrop:j["default"].PropTypes.oneOf(["static",!0,!1]),keyboard:j["default"].PropTypes.bool,animation:j["default"].PropTypes.bool,dialogComponentClass:A["default"], p["default"])({},P["default"].propTypes,H["default"].propTypes,{backdrop:j["default"].PropTypes.oneOf(["static",!0,!1]),keyboard:j["default"].PropTypes.bool,animation:j["default"].PropTypes.bool,dialogComponentClass:A["default"],
autoFocus:j["default"].PropTypes.bool,enforceFocus:j["default"].PropTypes.bool,show:j["default"].PropTypes.bool,onHide:j["default"].PropTypes.func,onEnter:j["default"].PropTypes.func,onEntering:j["default"].PropTypes.func, autoFocus:j["default"].PropTypes.bool,enforceFocus:j["default"].PropTypes.bool,show:j["default"].PropTypes.bool,onHide:j["default"].PropTypes.func,onEnter:j["default"].PropTypes.func,onEntering:j["default"].PropTypes.func,
onEntered:j["default"].PropTypes.func,onExit:j["default"].PropTypes.func,onExiting:j["default"].PropTypes.func,onExited:j["default"].PropTypes.func,container:P["default"].propTypes.container}),J=(0,p["default"])({},P["default"].defaultProps,{ onEntered:j["default"].PropTypes.func,onExit:j["default"].PropTypes.func,onExiting:j["default"].PropTypes.func,onExited:j["default"].PropTypes.func,container:P["default"].propTypes.container}),J=(0,p["default"])({},P["default"].defaultProps,{
@ -8988,7 +8988,7 @@ var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){re
e["default"]=(0,u["default"])(i)},function(t,e,n){"use strict" e["default"]=(0,u["default"])(i)},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}) function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0})
var i=n(5),o=r(i),a=n(176),s=r(a),l=n(1180),u=r(l),c=n(1145),d=r(c),f=n(1182),p=r(f),h=o["default"].createClass({displayName:"Portal",propTypes:{container:o["default"].PropTypes.oneOfType([u["default"],o["default"].PropTypes.func]) var i=n(5),o=r(i),a=n(177),s=r(a),l=n(1180),u=r(l),c=n(1145),d=r(c),f=n(1182),p=r(f),h=o["default"].createClass({displayName:"Portal",propTypes:{container:o["default"].PropTypes.oneOfType([u["default"],o["default"].PropTypes.func])
},componentDidMount:function m(){this._renderOverlay()},componentDidUpdate:function v(){this._renderOverlay()},componentWillReceiveProps:function g(t){this._overlayTarget&&t.container!==this.props.container&&(this._portalContainerNode.removeChild(this._overlayTarget), },componentDidMount:function m(){this._renderOverlay()},componentDidUpdate:function v(){this._renderOverlay()},componentWillReceiveProps:function g(t){this._overlayTarget&&t.container!==this.props.container&&(this._portalContainerNode.removeChild(this._overlayTarget),
this._portalContainerNode=(0,p["default"])(t.container,(0,d["default"])(this).body),this._portalContainerNode.appendChild(this._overlayTarget))},componentWillUnmount:function y(){this._unrenderOverlay(), this._portalContainerNode=(0,p["default"])(t.container,(0,d["default"])(this).body),this._portalContainerNode.appendChild(this._overlayTarget))},componentWillUnmount:function y(){this._unrenderOverlay(),
this._unmountOverlayTarget()},_mountOverlayTarget:function _(){this._overlayTarget||(this._overlayTarget=document.createElement("div"),this._portalContainerNode=(0,p["default"])(this.props.container,(0, this._unmountOverlayTarget()},_mountOverlayTarget:function _(){this._overlayTarget||(this._overlayTarget=document.createElement("div"),this._portalContainerNode=(0,p["default"])(this.props.container,(0,
@ -9000,7 +9000,7 @@ if(!this.isMounted())throw new Error("getOverlayDOMNode(): A component must be m
return this._overlayInstance?this._overlayInstance.getWrappedDOMNode?this._overlayInstance.getWrappedDOMNode():s["default"].findDOMNode(this._overlayInstance):null}}) return this._overlayInstance?this._overlayInstance.getWrappedDOMNode?this._overlayInstance.getWrappedDOMNode():s["default"].findDOMNode(this._overlayInstance):null}})
e["default"]=h,t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=h,t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return t="function"==typeof t?t():t,a["default"].findDOMNode(t)||e}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return t="function"==typeof t?t():t,a["default"].findDOMNode(t)||e}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i
var o=n(176),a=r(o) var o=n(177),a=r(o)
t.exports=e["default"]},function(t,e,n){"use strict" t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){var n=-1 function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){var n=-1
return t.some(function(t,r){if(e(t,r))return n=r,!0}),n}function a(t,e){return o(t,function(t){return t.modals.indexOf(e)!==-1})}Object.defineProperty(e,"__esModule",{value:!0}) return t.some(function(t,r){if(e(t,r))return n=r,!0}),n}function a(t,e){return o(t,function(t){return t.modals.indexOf(e)!==-1})}Object.defineProperty(e,"__esModule",{value:!0})
@ -9074,7 +9074,7 @@ var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064)
return g["default"].createElement("h4",(0,o["default"])({},a,{className:(0,m["default"])(e,l)}))},e}(g["default"].Component) return g["default"].createElement("h4",(0,o["default"])({},a,{className:(0,m["default"])(e,l)}))},e}(g["default"].Component)
e["default"]=(0,y.bsClass)("modal-title",_),t.exports=e["default"]},function(t,e,n){"use strict" e["default"]=(0,y.bsClass)("modal-title",_),t.exports=e["default"]},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}e.__esModule=!0
var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(1126),g=r(v),y=n(5),_=r(y),b=n(176),x=r(b),w=n(1096),k=r(w),C=n(1104),j=r(C),T=n(1075),E=n(1082),S=r(E),P=n(1083),O=r(P),M={ var i=n(989),o=r(i),a=n(1073),s=r(a),l=n(1027),u=r(l),c=n(1028),d=r(c),f=n(1064),p=r(f),h=n(1074),m=r(h),v=n(1126),g=r(v),y=n(5),_=r(y),b=n(177),x=r(b),w=n(1096),k=r(w),C=n(1104),j=r(C),T=n(1075),E=n(1082),S=r(E),P=n(1083),O=r(P),M={
activeKey:_["default"].PropTypes.any,activeHref:_["default"].PropTypes.string,stacked:_["default"].PropTypes.bool,justified:(0,k["default"])(_["default"].PropTypes.bool,function(t){var e=t.justified,n=t.navbar activeKey:_["default"].PropTypes.any,activeHref:_["default"].PropTypes.string,stacked:_["default"].PropTypes.bool,justified:(0,k["default"])(_["default"].PropTypes.bool,function(t){var e=t.justified,n=t.navbar
@ -9235,7 +9235,7 @@ t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,wri
value:!0}) value:!0})
var l=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e] var l=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]
for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},u=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n] for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},u=function(){function t(t,e){for(var n=0;n<e.length;n++){var r=e[n]
r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),c=n(1074),d=r(c),f=n(5),p=r(f),h=n(176),m=r(h),v=n(1180),g=r(v),y=n(1208),_=r(y),b=n(1182),x=r(b),w=n(1145),k=r(w),C=function(t){ r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,n,r){return n&&t(e.prototype,n),r&&t(e,r),e}}(),c=n(1074),d=r(c),f=n(5),p=r(f),h=n(177),m=r(h),v=n(1180),g=r(v),y=n(1208),_=r(y),b=n(1182),x=r(b),w=n(1145),k=r(w),C=function(t){
function e(t,n){o(this,e) function e(t,n){o(this,e)
var r=a(this,Object.getPrototypeOf(e).call(this,t,n)) var r=a(this,Object.getPrototypeOf(e).call(this,t,n))
return r.state={positionLeft:0,positionTop:0,arrowOffsetLeft:null,arrowOffsetTop:null},r._needsFlush=!1,r._lastTarget=null,r}return s(e,t),u(e,[{key:"componentDidMount",value:function n(){this.updatePosition(this.getTarget()) return r.state={positionLeft:0,positionTop:0,arrowOffsetLeft:null,arrowOffsetTop:null},r._needsFlush=!1,r._lastTarget=null,r}return s(e,t),u(e,[{key:"componentDidMount",value:function n(){this.updatePosition(this.getTarget())
@ -9296,7 +9296,7 @@ return void 0===e?n?"pageXOffset"in n?n.pageXOffset:n.document.documentElement.s
}},function(t,e,n){"use strict" }},function(t,e,n){"use strict"
function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return Array.isArray(e)?e.indexOf(t)>=0:t===e}e.__esModule=!0 function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return Array.isArray(e)?e.indexOf(t)>=0:t===e}e.__esModule=!0
var o=n(1073),a=r(o),s=n(1027),l=r(s),u=n(1028),c=r(u),d=n(1064),f=r(d),p=n(989),h=r(p),m=n(1125),v=r(m),g=n(5),y=r(g),_=n(176),b=r(_),x=n(1104),w=r(x),k=n(1205),C=r(k),j=n(1082),T=r(j),E=y["default"].PropTypes.oneOf(["click","hover","focus"]),S=(0, var o=n(1073),a=r(o),s=n(1027),l=r(s),u=n(1028),c=r(u),d=n(1064),f=r(d),p=n(989),h=r(p),m=n(1125),v=r(m),g=n(5),y=r(g),_=n(177),b=r(_),x=n(1104),w=r(x),k=n(1205),C=r(k),j=n(1082),T=r(j),E=y["default"].PropTypes.oneOf(["click","hover","focus"]),S=(0,
h["default"])({},C["default"].propTypes,{trigger:y["default"].PropTypes.oneOfType([E,y["default"].PropTypes.arrayOf(E)]),delay:y["default"].PropTypes.number,delayShow:y["default"].PropTypes.number,delayHide:y["default"].PropTypes.number, h["default"])({},C["default"].propTypes,{trigger:y["default"].PropTypes.oneOfType([E,y["default"].PropTypes.arrayOf(E)]),delay:y["default"].PropTypes.number,delayShow:y["default"].PropTypes.number,delayHide:y["default"].PropTypes.number,
defaultOverlayShown:y["default"].PropTypes.bool,overlay:y["default"].PropTypes.node.isRequired,onBlur:y["default"].PropTypes.func,onClick:y["default"].PropTypes.func,onFocus:y["default"].PropTypes.func, defaultOverlayShown:y["default"].PropTypes.bool,overlay:y["default"].PropTypes.node.isRequired,onBlur:y["default"].PropTypes.func,onClick:y["default"].PropTypes.func,onFocus:y["default"].PropTypes.func,
onMouseOut:y["default"].PropTypes.func,onMouseOver:y["default"].PropTypes.func,target:y["default"].PropTypes.oneOf([null]),onHide:y["default"].PropTypes.oneOf([null]),show:y["default"].PropTypes.oneOf([null]) onMouseOut:y["default"].PropTypes.func,onMouseOver:y["default"].PropTypes.func,target:y["default"].PropTypes.oneOf([null]),onHide:y["default"].PropTypes.oneOf([null]),show:y["default"].PropTypes.oneOf([null])

View File

@ -36,6 +36,7 @@ class FormAlert extends SilverStripeComponent {
* @returns {string} can be the following values "success", "warning", "danger", "info" * @returns {string} can be the following values "success", "warning", "danger", "info"
*/ */
getMessageStyle() { getMessageStyle() {
// See ValidationResult::TYPE_ constant definitions in PHP.
switch (this.props.type) { switch (this.props.type) {
case 'good': case 'good':
case 'success': case 'success':

View File

@ -103,8 +103,9 @@ class FormBuilder extends SilverStripeComponent {
const dataWithAction = Object.assign({}, data, { const dataWithAction = Object.assign({}, data, {
[action]: 1, [action]: 1,
}); });
const requestedSchema = this.props.responseRequestedSchema.join();
const headers = { const headers = {
'X-Formschema-Request': 'state,schema', 'X-Formschema-Request': requestedSchema,
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
}; };
@ -383,6 +384,9 @@ const basePropTypes = {
submitting: PropTypes.bool, submitting: PropTypes.bool,
baseFormComponent: PropTypes.func.isRequired, baseFormComponent: PropTypes.func.isRequired,
baseFieldComponent: PropTypes.func.isRequired, baseFieldComponent: PropTypes.func.isRequired,
responseRequestedSchema: PropTypes.arrayOf(PropTypes.oneOf([
'schema', 'state', 'errors', 'auto',
])),
}; };
FormBuilder.propTypes = Object.assign({}, basePropTypes, { FormBuilder.propTypes = Object.assign({}, basePropTypes, {
@ -390,5 +394,9 @@ FormBuilder.propTypes = Object.assign({}, basePropTypes, {
schema: schemaPropType.isRequired, schema: schemaPropType.isRequired,
}); });
FormBuilder.defaultProps = {
responseRequestedSchema: ['auto'],
};
export { basePropTypes, schemaPropType }; export { basePropTypes, schemaPropType };
export default FormBuilder; export default FormBuilder;

View File

@ -38,6 +38,25 @@ If you want to load the schema from a server via XHR, use the
* `touchOnChange` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/) * `touchOnChange` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
* `persistentSubmitErrors` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/) * `persistentSubmitErrors` (bool): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
* `validate` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/) * `validate` (function): See [redux-form](http://redux-form.com/6.0.5/docs/api/ReduxForm.md/)
* `responseRequestedSchema` (array): This allows you to customise the response requested from the server
on submit. See below on "Handling submissions".
## Handling submissions
The `responseRequestedSchema` property will control the value of the 'X-Formschema-Request' header, which
in turn communicates to PHP the kind of response react would like. Your form should only specify the
bare minimum that it requires, as each header will represent additional overhead on all XHR requests.
This is an array which may be any combination of the below values:
* `schema`: The schema is requested on submit
* `state`: The state is requested on submit. Note that this may also include form errors.
* `errors`: The list of validation errors is returned in case of error.
* `auto`: (default) Conditionally return `errors` if there are errors, or `state` if there are none.
Note that these are only the requested header values; The PHP submission method may choose to ignore
these values, and return any combination of the above. Typically the only time this requested value
is respected is when handled by the default validation error handler (LeftAndMain::getSchemaResponse)
## Schema Structure ## Schema Structure

View File

@ -2,6 +2,7 @@ import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import fetch from 'isomorphic-fetch'; import fetch from 'isomorphic-fetch';
import deepFreeze from 'deep-freeze-strict';
import { import {
Field as ReduxFormField, Field as ReduxFormField,
reduxForm, reduxForm,
@ -20,6 +21,7 @@ class FormBuilderLoader extends Component {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.clearSchema = this.clearSchema.bind(this); this.clearSchema = this.clearSchema.bind(this);
this.reduceSchemaErrors = this.reduceSchemaErrors.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -90,10 +92,13 @@ class FormBuilderLoader extends Component {
return promise return promise
.then(formSchema => { .then(formSchema => {
if (formSchema) { let schema = formSchema;
this.props.schemaActions.setSchema(this.props.schemaUrl, formSchema); if (schema) {
// Strip errors out of schema response in preparation for setSchema and SubmissionError
schema = this.reduceSchemaErrors(schema);
this.props.schemaActions.setSchema(this.props.schemaUrl, schema);
} }
return formSchema; return schema;
}) })
// TODO Suggest storing messages in a separate redux store rather than throw an error // TODO Suggest storing messages in a separate redux store rather than throw an error
// ref: https://github.com/erikras/redux-form/issues/94#issuecomment-143398399 // ref: https://github.com/erikras/redux-form/issues/94#issuecomment-143398399
@ -110,27 +115,40 @@ class FormBuilderLoader extends Component {
}); });
} }
overrideStateData(state) { /**
if (!this.props.stateOverrides || !state) { * Given a submitted schema, ensure that any errors property is merged safely into
return state; * the state.
*
* @param {Object} schema - New schema result
* @return {Object}
*/
reduceSchemaErrors(schema) {
// Skip if there are no errors
if (!schema.errors) {
return schema;
} }
const fieldOverrides = this.props.stateOverrides.fields;
let fields = state.fields; // Inherit state from current schema if not being assigned in this request
if (fieldOverrides && fields) { let reduced = Object.assign({}, schema);
fields = fields.map((field) => { if (!reduced.state) {
const fieldOverride = fieldOverrides.find((override) => override.name === field.name); reduced = Object.assign({}, reduced, { state: this.props.schema.state });
if (!fieldOverride) {
return field;
}
// need to be recursive for the unknown-sized "data" properly
return merge.recursive(true, field, fieldOverride);
});
} }
return Object.assign({},
state, // Modify state.fields and replace state.messages
this.props.stateOverrides, reduced = Object.assign({}, reduced, {
{ fields } state: Object.assign({}, reduced.state, {
); // Replace message property for each field
fields: reduced.state.fields.map((field) => Object.assign({}, field, {
message: schema.errors.find((error) => error.field === field.name),
})),
// Non-field messages
messages: schema.errors.filter((error) => !error.field),
}),
});
// Can be safely discarded
delete reduced.errors;
return deepFreeze(reduced);
} }
/** /**
@ -173,6 +191,7 @@ class FormBuilderLoader extends Component {
* @return {Object} Promise from the AJAX request. * @return {Object} Promise from the AJAX request.
*/ */
fetch(schema = true, state = true) { fetch(schema = true, state = true) {
// Note: `errors` is only valid for submissions, not schema requests, so omitted here
const headerValues = []; const headerValues = [];
if (schema) { if (schema) {

View File

@ -8,11 +8,7 @@ export default function schemaReducer(state = initialState, action = null) {
case ACTION_TYPES.SET_SCHEMA: { case ACTION_TYPES.SET_SCHEMA: {
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
[action.payload.id]: Object.assign({}, state[action.payload.id], { [action.payload.id]: Object.assign({}, state[action.payload.id], action.payload),
id: action.payload.id,
schema: action.payload.schema,
state: action.payload.state,
}),
})); }));
} }

View File

@ -10,7 +10,9 @@ use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\Versioning\ChangeSet; use SilverStripe\ORM\Versioning\ChangeSet;
use SilverStripe\ORM\Versioning\ChangeSetItem; use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
@ -437,19 +439,22 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
->setIcon('save'), ->setIcon('save'),
FormAction::create('cancel', _t('LeftAndMain.CANCEL', 'Cancel')) FormAction::create('cancel', _t('LeftAndMain.CANCEL', 'Cancel'))
->setUseButtonTag(true) ->setUseButtonTag(true)
) ),
new RequiredFields('Name')
); );
// Load into form // Load into form
if($id && $record) { if($id && $record) {
$form->loadDataFrom($record); $form->loadDataFrom($record);
} }
$form->getValidator()->addRequiredField('Name');
// Configure form to respond to validation errors with form schema // Configure form to respond to validation errors with form schema
// if requested via react. // if requested via react.
$form->setValidationResponseCallback(function() use ($form, $record) { $form->setValidationResponseCallback(function(ValidationResult $errors) use ($form, $record) {
$schemaId = Controller::join_links($this->Link('schema/DetailEditForm'), $record->exists() ? $record->ID : ''); $schemaId = Controller::join_links(
return $this->getSchemaResponse($form, $schemaId); $this->Link('schema/DetailEditForm'),
$record->isInDB() ? $record->ID : ''
);
return $this->getSchemaResponse($schemaId, $form, $errors);
}); });
return $form; return $form;

View File

@ -35,6 +35,7 @@ use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\Versioning\Versioned; use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
@ -55,18 +56,20 @@ use InvalidArgumentException;
use SilverStripe\SiteConfig\SiteConfig; use SilverStripe\SiteConfig\SiteConfig;
/** /**
* LeftAndMain is the parent class of all the two-pane views in the CMS. * LeftAndMain is the parent class of all the two-pane views in the CMS.
* If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain. * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
* *
* This is essentially an abstract class which should be subclassed. * This is essentially an abstract class which should be subclassed.
* See {@link CMSMain} for a good example. * See {@link CMSMain} for a good example.
*
* @property FormSchema $schema
*/ */
class LeftAndMain extends Controller implements PermissionProvider { class LeftAndMain extends Controller implements PermissionProvider {
/**
* Form schema header identifier
*/
const SCHEMA_HEADER = 'X-Formschema-Request';
/** /**
* Enable front-end debugging (increases verbosity) in dev mode. * Enable front-end debugging (increases verbosity) in dev mode.
* Will be ignored in live environments. * Will be ignored in live environments.
@ -156,9 +159,16 @@ class LeftAndMain extends Controller implements PermissionProvider {
]; ];
private static $dependencies = [ private static $dependencies = [
'schema' => '%$FormSchema' 'FormSchema' => '%$FormSchema'
]; ];
/**
* Current form schema helper
*
* @var FormSchema
*/
protected $schema = null;
/** /**
* Assign themes to use for cms * Assign themes to use for cms
* *
@ -296,6 +306,26 @@ class LeftAndMain extends Controller implements PermissionProvider {
]; ];
} }
/**
* Get form schema helper
*
* @return FormSchema
*/
public function getFormSchema() {
return $this->schema;
}
/**
* Set form schema helper for this controller
*
* @param FormSchema $schema
* @return $this
*/
public function setFormSchema(FormSchema $schema) {
$this->schema = $schema;
return $this;
}
/** /**
* Gets a JSON schema representing the current edit form. * Gets a JSON schema representing the current edit form.
* *
@ -305,7 +335,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @return HTTPResponse * @return HTTPResponse
*/ */
public function schema($request) { public function schema($request) {
$response = $this->getResponse();
$formName = $request->param('FormName'); $formName = $request->param('FormName');
$itemID = $request->param('ItemID'); $itemID = $request->param('ItemID');
@ -322,72 +351,43 @@ class LeftAndMain extends Controller implements PermissionProvider {
} }
$form = $this->{"get{$formName}"}($itemID); $form = $this->{"get{$formName}"}($itemID);
$schemaID = $request->getURL();
$response->addHeader('Content-Type', 'application/json'); return $this->getSchemaResponse($schemaID, $form);
$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
return $response;
} }
/**
* Check if the current request has a X-Formschema-Request header set.
* Used by conditional logic that responds to validation results
*
* @return bool
*/
protected function getSchemaRequested() {
$parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
return !empty($parts);
}
/** /**
* Given a form, generate a response containing the requested form * Generate schema for the given form based on the X-Formschema-Request header value
* schema if X-Formschema-Request header is set.
* *
* @param Form $form * @param string $schemaID ID for this schema. Required.
* @param String $id Optional, will default to the current request URL * @param Form $form Required for 'state' or 'schema' response
* @param ValidationResult $errors Required for 'error' response
* @param array $extraData Any extra data to be merged with the schema response
* @return HTTPResponse * @return HTTPResponse
*/ */
protected function getSchemaResponse($form, $id = null) { protected function getSchemaResponse($schemaID, $form = null, ValidationResult $errors = null, $extraData = []) {
$request = $this->getRequest(); $parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
if($request->getHeader('X-Formschema-Request')) { $data = $this
$data = $this->getSchemaForForm($form, $id); ->getFormSchema()
$response = new HTTPResponse(Convert::raw2json($data)); ->getMultipartSchema($parts, $schemaID, $form, $errors);
$response->addHeader('Content-Type', 'application/json');
// Clear non-schema form validation / data / message if ($extraData) {
// since it does not need to be redirected $data = array_merge($data, $extraData);
$form->clearMessage(); }
return $response;
}
return null;
}
/** $response = new HTTPResponse(Convert::raw2json($data));
* Returns a representation of the provided {@link Form} as structured data, $response->addHeader('Content-Type', 'application/json');
* based on the request data. return $response;
*
* @param Form $form
* @param String $id Optional, will default to the current request URL
* @return array
*/
protected function getSchemaForForm(Form $form, $id = null) {
$request = $this->getRequest();
$id = $id ? $id : $request->getURL();
$return = null;
// Valid values for the "X-Formschema-Request" header are "schema" and "state".
// If either of these values are set they will be stored in the $schemaParst array
// and used to construct the response body.
if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
$validHeaderValues = ['schema', 'state'];
return in_array(trim($value), $validHeaderValues);
});
} else {
$schemaParts = ['schema'];
}
$return = ['id' => $id];
if (in_array('schema', $schemaParts)) {
$return['schema'] = $this->schema->getSchema($form);
}
if (in_array('state', $schemaParts)) {
$return['state'] = $this->schema->getState($form);
}
return $return;
} }
/** /**
@ -1304,14 +1304,12 @@ class LeftAndMain extends Controller implements PermissionProvider {
$this->setCurrentPageID($record->ID); $this->setCurrentPageID($record->ID);
$message = _t('LeftAndMain.SAVEDUP', 'Saved.'); $message = _t('LeftAndMain.SAVEDUP', 'Saved.');
if($request->getHeader('X-Formschema-Request')) { if($this->getSchemaRequested()) {
$schemaId = Controller::join_links($this->Link('schema/DetailEditForm'), $id); $schemaId = Controller::join_links($this->Link('schema/DetailEditForm'), $id);
// Ensure that newly created records have all their data loaded back into the form. // Ensure that newly created records have all their data loaded back into the form.
$form->loadDataFrom($record); $form->loadDataFrom($record);
$form->setMessage($message, 'good'); $form->setMessage($message, 'good');
$data = $this->getSchemaForForm($form, $schemaId); $response = $this->getSchemaResponse($schemaId, $form);
$response = new HTTPResponse(Convert::raw2json($data));
$response->addHeader('Content-Type', 'application/json');
} else { } else {
$response = $this->getResponseNegotiator()->respond($request); $response = $this->getResponseNegotiator()->respond($request);
} }
@ -1580,10 +1578,9 @@ class LeftAndMain extends Controller implements PermissionProvider {
$form->loadDataFrom($record); $form->loadDataFrom($record);
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
$form->setAttribute('data-pjax-fragment', 'CurrentForm'); $form->setAttribute('data-pjax-fragment', 'CurrentForm');
$form->setValidationResponseCallback(function() use ($negotiator, $form) { $form->setValidationResponseCallback(function(ValidationResult $errors) use ($negotiator, $form) {
$request = $this->getRequest(); $request = $this->getRequest();
if($request->isAjax() && $negotiator) { if($request->isAjax() && $negotiator) {
$form->setupFormErrors();
$result = $form->forTemplate(); $result = $form->forTemplate();
return $negotiator->respond($request, array( return $negotiator->respond($request, array(

View File

@ -65,7 +65,8 @@ the `setValue` method.
:::php :::php
public function validate($validator) { public function validate($validator) {
if($this->value == 10) { if($this->Value() == 10) {
$validator->validationError($this->Name(), 'This value cannot be 10');
return false; return false;
} }
@ -73,7 +74,7 @@ the `setValue` method.
} }
The `validate` method should return `true` if the value passes any validation and `false` if SilverStripe should trigger The `validate` method should return `true` if the value passes any validation and `false` if SilverStripe should trigger
a validation error on the page. a validation error on the page. In addition a useful error message must be set on the given validator.
<div class="notice" markdown="1"> <div class="notice" markdown="1">
You can also override the entire `Form` validation by subclassing `Form` and defining a `validate` method on the form. You can also override the entire `Form` validation by subclassing `Form` and defining a `validate` method on the form.
@ -141,7 +142,7 @@ reusable and would not be possible within the `CMS` or other automated `UI` but
} }
public function doSubmitForm($data, $form) { public function doSubmitForm($data, $form) {
// At this point, RequiredFields->validate() will have been called already, // At this point, RequiredFields->isValid() will have been called already,
// so we can assume that the values exist. Say we want to make sure that email hasn't already been used. // so we can assume that the values exist. Say we want to make sure that email hasn't already been used.
$check = Member::get()->filter('Email', $data['Email'])->first(); $check = Member::get()->filter('Email', $data['Email'])->first();
@ -216,6 +217,35 @@ An alternative (or additional) approach to validation is to place it directly on
provides a [api:DataObject::validate()] method to validate data at the model level. See provides a [api:DataObject::validate()] method to validate data at the model level. See
[Data Model Validation](../model/validation). [Data Model Validation](../model/validation).
## Form action validation
At times it's not possible for all validation or recoverable errors to be pre-determined in advance of form
submission, such as those generated by the form [api:Validator] object. Sometimes errors may occur within form
action methods, and it is necessary to display errors on the form after initial validation has been performed.
In this case you may throw a [api:ValidationException] object within your handler, optionally passing it an
error message, or a [api:ValidationResult] object containing the list of errors you wish to display.
E.g.
:::php
class MyController extends Controller
{
public function doSave($data, $form) {
$success = $this->sendEmail($data);
// Example error handling
if (!$success) {
throw new ValidationException('Sorry, we could not email to that address');
}
// If success
return $this->redirect($this->Link('success'));
}
}
### Validation in the CMS ### Validation in the CMS
In the CMS, we're not creating the forms for editing CMS records. The `Form` instance is generated for us so we cannot In the CMS, we're not creating the forms for editing CMS records. The `Form` instance is generated for us so we cannot

View File

@ -1095,6 +1095,50 @@ Some methods on `Requirements` have had their method signatures changed:
A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be
forced on during development. If this is off, combined files is only enabled in live environments. forced on during development. If this is off, combined files is only enabled in live environments.
Form validation has been refactored significantly. A new `FormMessage` trait has been created to
handle field-level and form-level messages. This has the following properties:
* `setMessage` to assign a message, type, and cast
* `getMessage` retrieves the message string
* `getMessageType` retrieves the message type (E.g. error, good, info)
* `getMessageCast` retrieves the cast type
* `getMessageCastingHelper` retrieves the DBField cast to use for the appropriate message cast
* `getSchemaMessage` encodes this message for form schema use in ReactJS.
`Form` methods have been changed:
* `validate` is replaced with `validationResult` instead, which returns a `ValidationResult` instance.
This is no longer automatically persisted in the state by default, unless a redirection occurs.
You can also save any response in the state by manually invoking `saveFormState` inside a custom
validation response handler.
* `setupFormErrors` renamed to `restoreFormState`
* `resetValidation` renamed to `clearFormState`
* `loadMessagesFrom` method created to load a ValidationResult into a form.
* `setMessage`. third parameter is now $cast type
* `messageForForm` removed. Use `setMessage` or `sessionMessage` instead.
* `getSessionValidationResult` / `setSessionValidationResult` used to get / set session errors
* `getSessionData` / `setSessionData` used to get / set field values cached in the session
* `getAjaxErrorResponse` and `getRedirectReferer` created to simplify `getValidationErrorResponse`
* `addErrorMessage` removed. Users can either use `sessionMessage` or `sessionError` to add a
form level message, throw a ValidationException during submission, or add a custom validator.
`Validator` methods have changed:
* `validate` method now returns a `ValidationResult` instance.
* `requireField` method removed. Use `RequiredFields` subclass instead.
`ValidationResult` now has these methods:
* `serialize` / `unserialize` for saving within session state
* `messageList` renamed to `getMessages`
* `error` method replaced with `addMessage` / `addError` / `addFieldMessage` / `addFieldError`
* `valid` renamed to `isValid`
`ValidationException` has these changes:
* `$message` second constructor parameter is removed. Constructor only accepts `$result`,
which may be a string, and optional `$code`
#### <a name="overview-template-removed"></a>Template and Form Removed API #### <a name="overview-template-removed"></a>Template and Form Removed API
* Removed `TabularStyle` * Removed `TabularStyle`
@ -1103,12 +1147,24 @@ forced on during development. If this is off, combined files is only enabled in
* `getTabPathRewrites` * `getTabPathRewrites`
* `setTabPathRewrites` * `setTabPathRewrites`
* `rewriteTabPath` * `rewriteTabPath`
* Removed `Form` methods: * Removed `Form` methods (see above for replacements):
* `transformTo` * `transformTo`
* `callfieldmethod` * `callfieldmethod`
* `single_field_required` * `single_field_required`
* `current_action` * `current_action`
* `set_current_action` * `set_current_action`
* `setupFormErrors`
* `resetValidation`
* `messageForForm`
* `addErrorMessage`
* Removed `Validator::requireField()` method.
* Removed `ValidationResult` (see above for replacements):
* `messageList`
* `codeList`
* `message`
* `starredList`
* `error`
* `valid`
* Removed `ReportAdminForm.ss` template * Removed `ReportAdminForm.ss` template
* `FormField::dontEscape()` has been removed. Escaping is now managed on a class by class basis. * `FormField::dontEscape()` has been removed. Escaping is now managed on a class by class basis.
* Removed `PermissionCheckboxSetField::getAssignedPermissionCodes()` (never implemented) * Removed `PermissionCheckboxSetField::getAssignedPermissionCodes()` (never implemented)

View File

@ -84,9 +84,9 @@ Each release is labeled in the format `$MAJOR`.`$MINOR`.`$PATCH`. For example, 3
* `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API. * `$MAJOR` version is incremented if any backwards incompatible changes are introduced to the public API.
* `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or * `$MINOR` version is incremented if new, backwards compatible **functionality** is introduced to the public API or
improvements are introduced within the private code. improvements are introduced within the private code.
* `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as * `$PATCH` version is incremented if only backwards compatible **bug fixes** are introduced. A bug fix is defined as
an internal change that fixes incorrect behavior. an internal change that fixes incorrect behavior.
**Public API** refers to any aspect of the system that has been designed to be used by SilverStripe modules & site developers. In SilverStripe 3, because we haven't been clear, in principle we have to treat every public or protected method as *potentially* part of the public API, but sometimes it comes to a judgement call about how likely it is that a given method will have been used in a particular way. If we were strict about never changing publicly exposed behaviour, it would be difficult to fix any bug whatsoever, which isn't in the interests of our user community. **Public API** refers to any aspect of the system that has been designed to be used by SilverStripe modules & site developers. In SilverStripe 3, because we haven't been clear, in principle we have to treat every public or protected method as *potentially* part of the public API, but sometimes it comes to a judgement call about how likely it is that a given method will have been used in a particular way. If we were strict about never changing publicly exposed behaviour, it would be difficult to fix any bug whatsoever, which isn't in the interests of our user community.

View File

@ -12,7 +12,6 @@ use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Core\Convert;
/** /**
* Represents a file reference stored in a database * Represents a file reference stored in a database
@ -24,552 +23,541 @@ use SilverStripe\Core\Convert;
class DBFile extends DBComposite implements AssetContainer, Thumbnail class DBFile extends DBComposite implements AssetContainer, Thumbnail
{ {
use ImageManipulation; use ImageManipulation;
/** /**
* List of allowed file categories. * List of allowed file categories.
* *
* {@see File::$app_categories} * {@see File::$app_categories}
* *
* @var array * @var array
*/ */
protected $allowedCategories = array(); protected $allowedCategories = array();
/** /**
* List of image mime types supported by the image manipulations API * List of image mime types supported by the image manipulations API
* *
* {@see File::app_categories} for matching extensions. * {@see File::app_categories} for matching extensions.
* *
* @config * @config
* @var array * @var array
*/ */
private static $supported_images = array( private static $supported_images = array(
'image/jpeg', 'image/jpeg',
'image/gif', 'image/gif',
'image/png' 'image/png'
); );
/** /**
* Create a new image manipulation * Create a new image manipulation
* *
* @param string $name * @param string $name
* @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories * @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
*/ */
public function __construct($name = null, $allowed = array()) public function __construct($name = null, $allowed = array())
{ {
parent::__construct($name); parent::__construct($name);
$this->setAllowedCategories($allowed); $this->setAllowedCategories($allowed);
} }
/** /**
* Determine if a valid non-empty image exists behind this asset, which is a format * Determine if a valid non-empty image exists behind this asset, which is a format
* compatible with image manipulations * compatible with image manipulations
* *
* @return boolean * @return boolean
*/ */
public function getIsImage() public function getIsImage()
{ {
// Check file type // Check file type
$mime = $this->getMimeType(); $mime = $this->getMimeType();
return $mime && in_array($mime, $this->config()->supported_images); return $mime && in_array($mime, $this->config()->supported_images);
} }
/** /**
* @return AssetStore * @return AssetStore
*/ */
protected function getStore() protected function getStore()
{ {
return Injector::inst()->get('AssetStore'); return Injector::inst()->get('AssetStore');
} }
private static $composite_db = array( private static $composite_db = array(
"Hash" => "Varchar(255)", // SHA of the base content "Hash" => "Varchar(255)", // SHA of the base content
"Filename" => "Varchar(255)", // Path identifier of the base content "Filename" => "Varchar(255)", // Path identifier of the base content
"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given "Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
); );
private static $casting = array( private static $casting = array(
'URL' => 'Varchar', 'URL' => 'Varchar',
'AbsoluteURL' => 'Varchar', 'AbsoluteURL' => 'Varchar',
'Basename' => 'Varchar', 'Basename' => 'Varchar',
'Title' => 'Varchar', 'Title' => 'Varchar',
'MimeType' => 'Varchar', 'MimeType' => 'Varchar',
'String' => 'Text', 'String' => 'Text',
'Tag' => 'HTMLFragment', 'Tag' => 'HTMLFragment',
'Size' => 'Varchar' 'Size' => 'Varchar'
); );
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
return AssetField::create($this->getName(), $title); return AssetField::create($this->getName(), $title);
} }
/** /**
* Return a html5 tag of the appropriate for this file (normally img or a) * Return a html5 tag of the appropriate for this file (normally img or a)
* *
* @return string * @return string
*/ */
public function XML() public function XML()
{ {
return $this->getTag() ?: ''; return $this->getTag() ?: '';
} }
/** /**
* Return a html5 tag of the appropriate for this file (normally img or a) * Return a html5 tag of the appropriate for this file (normally img or a)
* *
* @return string * @return string
*/ */
public function getTag() public function getTag()
{ {
$template = $this->getFrontendTemplate(); $template = $this->getFrontendTemplate();
if(empty($template)) { if (empty($template)) {
return ''; return '';
} }
return (string)$this->renderWith($template); return (string)$this->renderWith($template);
} }
/** /**
* Determine the template to render as on the frontend * Determine the template to render as on the frontend
* *
* @return string Name of template * @return string Name of template
*/ */
public function getFrontendTemplate() public function getFrontendTemplate()
{ {
// Check that path is available // Check that path is available
$url = $this->getURL(); $url = $this->getURL();
if(empty($url)) { if (empty($url)) {
return null; return null;
} }
// Image template for supported images // Image template for supported images
if($this->getIsImage()) { if ($this->getIsImage()) {
return 'DBFile_image'; return 'DBFile_image';
} }
// Default download // Default download
return 'DBFile_download'; return 'DBFile_download';
} }
/** /**
* Get trailing part of filename * Get trailing part of filename
* *
* @return string * @return string
*/ */
public function getBasename() public function getBasename()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return basename($this->getSourceURL()); return basename($this->getSourceURL());
} }
/** /**
* Get file extension * Get file extension
* *
* @return string * @return string
*/ */
public function getExtension() public function getExtension()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return pathinfo($this->Filename, PATHINFO_EXTENSION); return pathinfo($this->Filename, PATHINFO_EXTENSION);
} }
/** /**
* Alt title for this * Alt title for this
* *
* @return string * @return string
*/ */
public function getTitle() public function getTitle()
{ {
// If customised, use the customised title // If customised, use the customised title
if($this->failover && ($title = $this->failover->Title)) { if ($this->failover && ($title = $this->failover->Title)) {
return $title; return $title;
} }
// fallback to using base name // fallback to using base name
return $this->getBasename(); return $this->getBasename();
} }
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array()) public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
{ {
$this->assertFilenameValid($filename ?: $path); $this->assertFilenameValid($filename ?: $path);
$result = $this $result = $this
->getStore() ->getStore()
->setFromLocalFile($path, $filename, $hash, $variant, $config); ->setFromLocalFile($path, $filename, $hash, $variant, $config);
// Update from result // Update from result
if($result) { if ($result) {
$this->setValue($result); $this->setValue($result);
} }
return $result; return $result;
} }
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array()) public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
{ {
$this->assertFilenameValid($filename); $this->assertFilenameValid($filename);
$result = $this $result = $this
->getStore() ->getStore()
->setFromStream($stream, $filename, $hash, $variant, $config); ->setFromStream($stream, $filename, $hash, $variant, $config);
// Update from result // Update from result
if($result) { if ($result) {
$this->setValue($result); $this->setValue($result);
} }
return $result; return $result;
} }
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array()) public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
{ {
$this->assertFilenameValid($filename); $this->assertFilenameValid($filename);
$result = $this $result = $this
->getStore() ->getStore()
->setFromString($data, $filename, $hash, $variant, $config); ->setFromString($data, $filename, $hash, $variant, $config);
// Update from result // Update from result
if($result) { if ($result) {
$this->setValue($result); $this->setValue($result);
} }
return $result; return $result;
} }
public function getStream() public function getStream()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
->getStore() ->getStore()
->getAsStream($this->Filename, $this->Hash, $this->Variant); ->getAsStream($this->Filename, $this->Hash, $this->Variant);
} }
public function getString() public function getString()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
->getStore() ->getStore()
->getAsString($this->Filename, $this->Hash, $this->Variant); ->getAsString($this->Filename, $this->Hash, $this->Variant);
} }
public function getURL($grant = true) public function getURL($grant = true)
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
$url = $this->getSourceURL($grant); $url = $this->getSourceURL($grant);
$this->updateURL($url); $this->updateURL($url);
$this->extend('updateURL', $url); $this->extend('updateURL', $url);
return $url; return $url;
} }
/** /**
* Get URL, but without resampling. * Get URL, but without resampling.
* Note that this will return the url even if the file does not exist. * Note that this will return the url even if the file does not exist.
* *
* @param bool $grant Ensures that the url for any protected assets is granted for the current user. * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* @return string * @return string
*/ */
public function getSourceURL($grant = true) public function getSourceURL($grant = true)
{ {
return $this return $this
->getStore() ->getStore()
->getAsURL($this->Filename, $this->Hash, $this->Variant, $grant); ->getAsURL($this->Filename, $this->Hash, $this->Variant, $grant);
} }
/** /**
* Get the absolute URL to this resource * Get the absolute URL to this resource
* *
* @return string * @return string
*/ */
public function getAbsoluteURL() public function getAbsoluteURL()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return Director::absoluteURL($this->getURL()); return Director::absoluteURL($this->getURL());
} }
public function getMetaData() public function getMetaData()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
->getStore() ->getStore()
->getMetadata($this->Filename, $this->Hash, $this->Variant); ->getMetadata($this->Filename, $this->Hash, $this->Variant);
} }
public function getMimeType() public function getMimeType()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
->getStore() ->getStore()
->getMimeType($this->Filename, $this->Hash, $this->Variant); ->getMimeType($this->Filename, $this->Hash, $this->Variant);
} }
public function getValue() public function getValue()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return array( return array(
'Filename' => $this->Filename, 'Filename' => $this->Filename,
'Hash' => $this->Hash, 'Hash' => $this->Hash,
'Variant' => $this->Variant 'Variant' => $this->Variant
); );
} }
public function getVisibility() public function getVisibility()
{ {
if(empty($this->Filename)) { if (empty($this->Filename)) {
return null; return null;
} }
return $this return $this
->getStore() ->getStore()
->getVisibility($this->Filename, $this->Hash); ->getVisibility($this->Filename, $this->Hash);
} }
public function exists() public function exists()
{ {
if(empty($this->Filename)) { if (empty($this->Filename)) {
return false; return false;
} }
return $this return $this
->getStore() ->getStore()
->exists($this->Filename, $this->Hash, $this->Variant); ->exists($this->Filename, $this->Hash, $this->Variant);
} }
public function getFilename() public function getFilename()
{ {
return $this->getField('Filename'); return $this->getField('Filename');
} }
public function getHash() public function getHash()
{ {
return $this->getField('Hash'); return $this->getField('Hash');
} }
public function getVariant() public function getVariant()
{ {
return $this->getField('Variant'); return $this->getField('Variant');
} }
/** /**
* Return file size in bytes. * Return file size in bytes.
* *
* @return int * @return int
*/ */
public function getAbsoluteSize() public function getAbsoluteSize()
{ {
$metadata = $this->getMetaData(); $metadata = $this->getMetaData();
if(isset($metadata['size'])) { if (isset($metadata['size'])) {
return $metadata['size']; return $metadata['size'];
} }
return 0; return 0;
} }
/** /**
* Customise this object with an "original" record for getting other customised fields * Customise this object with an "original" record for getting other customised fields
* *
* @param AssetContainer $original * @param AssetContainer $original
* @return $this * @return $this
*/ */
public function setOriginal($original) public function setOriginal($original)
{ {
$this->failover = $original; $this->failover = $original;
return $this; return $this;
} }
/** /**
* Get list of allowed file categories * Get list of allowed file categories
* *
* @return array * @return array
*/ */
public function getAllowedCategories() public function getAllowedCategories()
{ {
return $this->allowedCategories; return $this->allowedCategories;
} }
/** /**
* Assign allowed categories * Assign allowed categories
* *
* @param array|string $categories * @param array|string $categories
* @return $this * @return $this
*/ */
public function setAllowedCategories($categories) public function setAllowedCategories($categories)
{ {
if(is_string($categories)) { if (is_string($categories)) {
$categories = preg_split('/\s*,\s*/', $categories); $categories = preg_split('/\s*,\s*/', $categories);
} }
$this->allowedCategories = (array)$categories; $this->allowedCategories = (array)$categories;
return $this; return $this;
} }
/** /**
* Gets the list of extensions (if limited) for this field. Empty list * Gets the list of extensions (if limited) for this field. Empty list
* means there is no restriction on allowed types. * means there is no restriction on allowed types.
* *
* @return array * @return array
*/ */
protected function getAllowedExtensions() protected function getAllowedExtensions()
{ {
$categories = $this->getAllowedCategories(); $categories = $this->getAllowedCategories();
return File::get_category_extensions($categories); return File::get_category_extensions($categories);
} }
/** /**
* Validate that this DBFile accepts this filename as valid * Validate that this DBFile accepts this filename as valid
* *
* @param string $filename * @param string $filename
* @throws ValidationException * @throws ValidationException
* @return bool * @return bool
*/ */
protected function isValidFilename($filename) protected function isValidFilename($filename)
{ {
$extension = strtolower(File::get_file_extension($filename)); $extension = strtolower(File::get_file_extension($filename));
// Validate true if within the list of allowed extensions // Validate true if within the list of allowed extensions
$allowed = $this->getAllowedExtensions(); $allowed = $this->getAllowedExtensions();
if($allowed) { if ($allowed) {
return in_array($extension, $allowed); return in_array($extension, $allowed);
} }
// If no extensions are configured, fallback to global list // If no extensions are configured, fallback to global list
$globalList = File::config()->allowed_extensions; $globalList = File::config()->allowed_extensions;
if(in_array($extension, $globalList)) { if (in_array($extension, $globalList)) {
return true; return true;
} }
// Only admins can bypass global rules // Only admins can bypass global rules
return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN'); return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
} }
/** /**
* Check filename, and raise a ValidationException if invalid * Check filename, and raise a ValidationException if invalid
* *
* @param string $filename * @param string $filename
* @throws ValidationException * @throws ValidationException
*/ */
protected function assertFilenameValid($filename) protected function assertFilenameValid($filename)
{ {
$result = new ValidationResult(); $result = new ValidationResult();
$this->validate($result, $filename); $this->validate($result, $filename);
if(!$result->valid()) { if (!$result->isValid()) {
throw new ValidationException($result); throw new ValidationException($result);
} }
} }
/** /**
* Hook to validate this record against a validation result * Hook to validate this record against a validation result
* *
* @param ValidationResult $result * @param ValidationResult $result
* @param string $filename Optional filename to validate. If omitted, the current value is validated. * @param string $filename Optional filename to validate. If omitted, the current value is validated.
* @return bool Valid flag * @return bool Valid flag
*/ */
public function validate(ValidationResult $result, $filename = null) public function validate(ValidationResult $result, $filename = null)
{ {
if(empty($filename)) { if (empty($filename)) {
$filename = $this->getFilename(); $filename = $this->getFilename();
} }
if(empty($filename) || $this->isValidFilename($filename)) { if (empty($filename) || $this->isValidFilename($filename)) {
return true; return true;
} }
// Check allowed extensions $message = _t('File.INVALIDEXTENSIONSHORT', 'Extension is not allowed');
$extensions = $this->getAllowedExtensions(); $result->addError($message);
if(empty($extensions)) { return false;
$extensions = File::config()->allowed_extensions; }
}
sort($extensions);
$message = _t(
'File.INVALIDEXTENSION',
'Extension is not allowed (valid: {extensions})',
'Argument 1: Comma-separated list of valid extensions',
array('extensions' => wordwrap(implode(', ',$extensions)))
);
$result->addError($message);
return false;
}
public function setField($field, $value, $markChanged = true) public function setField($field, $value, $markChanged = true)
{ {
// Catch filename validation on direct assignment // Catch filename validation on direct assignment
if($field === 'Filename' && $value) { if ($field === 'Filename' && $value) {
$this->assertFilenameValid($value); $this->assertFilenameValid($value);
} }
return parent::setField($field, $value, $markChanged); return parent::setField($field, $value, $markChanged);
} }
/** /**
* Returns the size of the file type in an appropriate format. * Returns the size of the file type in an appropriate format.
* *
* @return string|false String value, or false if doesn't exist * @return string|false String value, or false if doesn't exist
*/ */
public function getSize() public function getSize()
{ {
$size = $this->getAbsoluteSize(); $size = $this->getAbsoluteSize();
if($size) { if ($size) {
return File::format_size($size); return File::format_size($size);
} }
return false; return false;
} }
public function deleteFile() public function deleteFile()
{ {
if(!$this->Filename) { if (!$this->Filename) {
return false; return false;
} }
return $this return $this
->getStore() ->getStore()
->delete($this->Filename, $this->Hash); ->delete($this->Filename, $this->Hash);
} }
public function publishFile() public function publishFile()
{ {
if($this->Filename) { if ($this->Filename) {
$this $this
->getStore() ->getStore()
->publish($this->Filename, $this->Hash); ->publish($this->Filename, $this->Hash);
} }
} }
public function protectFile() public function protectFile()
{ {
if($this->Filename) { if ($this->Filename) {
$this $this
->getStore() ->getStore()
->protect($this->Filename, $this->Hash); ->protect($this->Filename, $this->Hash);
} }
} }
public function grantFile() public function grantFile()
{ {
if($this->Filename) { if ($this->Filename) {
$this $this
->getStore() ->getStore()
->grant($this->Filename, $this->Hash); ->grant($this->Filename, $this->Hash);
} }
} }
public function revokeFile() public function revokeFile()
{ {
if($this->Filename) { if ($this->Filename) {
$this $this
->getStore() ->getStore()
->revoke($this->Filename, $this->Hash); ->revoke($this->Filename, $this->Hash);
} }
} }
public function canViewFile() public function canViewFile()
{ {
return $this->Filename return $this->Filename
&& $this && $this
->getStore() ->getStore()
->canView($this->Filename, $this->Hash); ->canView($this->Filename, $this->Hash);
} }
} }

View File

@ -33,374 +33,372 @@ use SimpleXMLElement;
*/ */
class FunctionalTest extends SapphireTest class FunctionalTest extends SapphireTest
{ {
/** /**
* Set this to true on your sub-class to disable the use of themes in this test. * Set this to true on your sub-class to disable the use of themes in this test.
* This can be handy for functional testing of modules without having to worry about whether a user has changed * This can be handy for functional testing of modules without having to worry about whether a user has changed
* behaviour by replacing the theme. * behaviour by replacing the theme.
* *
* @var bool * @var bool
*/ */
protected static $disable_themes = false; protected static $disable_themes = false;
/** /**
* Set this to true on your sub-class to use the draft site by default for every test in this class. * Set this to true on your sub-class to use the draft site by default for every test in this class.
* *
* @var bool * @var bool
*/ */
protected static $use_draft_site = false; protected static $use_draft_site = false;
/** /**
* @var TestSession * @var TestSession
*/ */
protected $mainSession = null; protected $mainSession = null;
/** /**
* CSSContentParser for the most recently requested page. * CSSContentParser for the most recently requested page.
* *
* @var CSSContentParser * @var CSSContentParser
*/ */
protected $cssParser = null; protected $cssParser = null;
/** /**
* If this is true, then 30x Location headers will be automatically followed. * If this is true, then 30x Location headers will be automatically followed.
* If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them. * If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them.
* However, this will let you inspect the intermediary headers * However, this will let you inspect the intermediary headers
* *
* @var bool * @var bool
*/ */
protected $autoFollowRedirection = true; protected $autoFollowRedirection = true;
/** /**
* Returns the {@link Session} object for this test * Returns the {@link Session} object for this test
* *
* @return Session * @return Session
*/ */
public function session() public function session()
{ {
return $this->mainSession->session(); return $this->mainSession->session();
} }
public function setUp() public function setUp()
{ {
// Skip calling FunctionalTest directly. // Skip calling FunctionalTest directly.
if(get_class($this) == __CLASS__) { if (get_class($this) == __CLASS__) {
$this->markTestSkipped(sprintf('Skipping %s ', get_class($this))); $this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
} }
parent::setUp(); parent::setUp();
$this->mainSession = new TestSession(); $this->mainSession = new TestSession();
// Disable theme, if necessary // Disable theme, if necessary
if(static::get_disable_themes()) { if (static::get_disable_themes()) {
SSViewer::config()->update('theme_enabled', false); SSViewer::config()->update('theme_enabled', false);
} }
// Switch to draft site, if necessary // Switch to draft site, if necessary
if(static::get_use_draft_site()) { if (static::get_use_draft_site()) {
$this->useDraftSite(); $this->useDraftSite();
} }
// Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case
// basis. // basis.
BasicAuth::protect_entire_site(false); BasicAuth::protect_entire_site(false);
SecurityToken::disable(); SecurityToken::disable();
} }
public function tearDown() public function tearDown()
{ {
SecurityToken::enable(); SecurityToken::enable();
parent::tearDown(); parent::tearDown();
unset($this->mainSession); unset($this->mainSession);
} }
/** /**
* Run a test while mocking the base url with the provided value * Run a test while mocking the base url with the provided value
* @param string $url The base URL to use for this test * @param string $url The base URL to use for this test
* @param callable $callback The test to run * @param callable $callback The test to run
*/ */
protected function withBaseURL($url, $callback) protected function withBaseURL($url, $callback)
{ {
$oldBase = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_url'); $oldBase = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_url');
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', $url); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', $url);
$callback($this); $callback($this);
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', $oldBase); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', $oldBase);
} }
/** /**
* Run a test while mocking the base folder with the provided value * Run a test while mocking the base folder with the provided value
* @param string $folder The base folder to use for this test * @param string $folder The base folder to use for this test
* @param callable $callback The test to run * @param callable $callback The test to run
*/ */
protected function withBaseFolder($folder, $callback) protected function withBaseFolder($folder, $callback)
{ {
$oldFolder = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_folder'); $oldFolder = Config::inst()->get('SilverStripe\\Control\\Director', 'alternate_base_folder');
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_folder', $folder); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_folder', $folder);
$callback($this); $callback($this);
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_folder', $oldFolder); Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_folder', $oldFolder);
} }
/** /**
* Submit a get request * Submit a get request
* @uses Director::test() * @uses Director::test()
* *
* @param string $url * @param string $url
* @param Session $session * @param Session $session
* @param array $headers * @param array $headers
* @param array $cookies * @param array $cookies
* @return HTTPResponse * @return HTTPResponse
*/ */
public function get($url, $session = null, $headers = null, $cookies = null) public function get($url, $session = null, $headers = null, $cookies = null)
{ {
$this->cssParser = null; $this->cssParser = null;
$response = $this->mainSession->get($url, $session, $headers, $cookies); $response = $this->mainSession->get($url, $session, $headers, $cookies);
if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
$response = $this->mainSession->followRedirection(); $response = $this->mainSession->followRedirection();
} }
return $response; return $response;
} }
/** /**
* Submit a post request * Submit a post request
* *
* @uses Director::test() * @uses Director::test()
* @param string $url * @param string $url
* @param array $data * @param array $data
* @param array $headers * @param array $headers
* @param Session $session * @param Session $session
* @param string $body * @param string $body
* @param array $cookies * @param array $cookies
* @return HTTPResponse * @return HTTPResponse
*/ */
public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null)
{ {
$this->cssParser = null; $this->cssParser = null;
$response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies); $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies);
if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
$response = $this->mainSession->followRedirection(); $response = $this->mainSession->followRedirection();
} }
return $response; return $response;
} }
/** /**
* Submit the form with the given HTML ID, filling it out with the given data. * Submit the form with the given HTML ID, filling it out with the given data.
* Acts on the most recent response. * Acts on the most recent response.
* *
* Any data parameters have to be present in the form, with exact form field name * Any data parameters have to be present in the form, with exact form field name
* and values, otherwise they are removed from the submission. * and values, otherwise they are removed from the submission.
* *
* Caution: Parameter names have to be formatted * Caution: Parameter names have to be formatted
* as they are in the form submission, not as they are interpreted by PHP. * as they are in the form submission, not as they are interpreted by PHP.
* Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two')) * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two'))
* Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two') * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two')
* *
* @see http://www.simpletest.org/en/form_testing_documentation.html * @see http://www.simpletest.org/en/form_testing_documentation.html
* *
* @param string $formID HTML 'id' attribute of a form (loaded through a previous response) * @param string $formID HTML 'id' attribute of a form (loaded through a previous response)
* @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute) * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute)
* @param array $data Map of GET/POST data. * @param array $data Map of GET/POST data.
* @return HTTPResponse * @return HTTPResponse
*/ */
public function submitForm($formID, $button = null, $data = array()) public function submitForm($formID, $button = null, $data = array())
{ {
$this->cssParser = null; $this->cssParser = null;
$response = $this->mainSession->submitForm($formID, $button, $data); $response = $this->mainSession->submitForm($formID, $button, $data);
if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) { if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
$response = $this->mainSession->followRedirection(); $response = $this->mainSession->followRedirection();
} }
return $response; return $response;
} }
/** /**
* Return the most recent content * Return the most recent content
* *
* @return string * @return string
*/ */
public function content() public function content()
{ {
return $this->mainSession->lastContent(); return $this->mainSession->lastContent();
} }
/** /**
* Find an attribute in a SimpleXMLElement object by name. * Find an attribute in a SimpleXMLElement object by name.
* @param SimpleXMLElement $object * @param SimpleXMLElement $object
* @param string $attribute Name of attribute to find * @param string $attribute Name of attribute to find
* @return SimpleXMLElement object of the attribute * @return SimpleXMLElement object of the attribute
*/ */
public function findAttribute($object, $attribute) public function findAttribute($object, $attribute)
{ {
$found = false; $found = false;
foreach($object->attributes() as $a => $b) { foreach ($object->attributes() as $a => $b) {
if($a == $attribute) { if ($a == $attribute) {
$found = $b; $found = $b;
} }
} }
return $found; return $found;
} }
/** /**
* Return a CSSContentParser for the most recent content. * Return a CSSContentParser for the most recent content.
* *
* @return CSSContentParser * @return CSSContentParser
*/ */
public function cssParser() public function cssParser()
{ {
if (!$this->cssParser) { if (!$this->cssParser) {
$this->cssParser = new CSSContentParser($this->mainSession->lastContent()); $this->cssParser = new CSSContentParser($this->mainSession->lastContent());
} }
return $this->cssParser; return $this->cssParser;
} }
/** /**
* Assert that the most recently queried page contains a number of content tags specified by a CSS selector. * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
* The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag
* will be examined. The assertion fails if one of the expectedMatches fails to appear. * will be examined. The assertion fails if one of the expectedMatches fails to appear.
* *
* Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account. * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
* *
* @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
* @param array|string $expectedMatches The content of at least one of the matched tags * @param array|string $expectedMatches The content of at least one of the matched tags
* @throws PHPUnit_Framework_AssertionFailedError * @param string $message
* @return boolean * @throws PHPUnit_Framework_AssertionFailedError
*/ */
public function assertPartialMatchBySelector($selector, $expectedMatches) public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null)
{ {
if (is_string($expectedMatches)) { if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches); $expectedMatches = array($expectedMatches);
} }
$items = $this->cssParser()->getBySelector($selector); $items = $this->cssParser()->getBySelector($selector);
$actuals = array(); $actuals = array();
if($items) foreach($items as $item) $actuals[trim(preg_replace("/\s+/", " ", (string)$item))] = true;
foreach($expectedMatches as $match) {
$this->assertTrue(
isset($actuals[$match]),
"Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
. implode("'\n'", $expectedMatches) . "'\n\n"
. "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"
);
return false;
}
return true;
}
/**
* Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
* The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag
* will be examined. The assertion fails if one of the expectedMatches fails to appear.
*
* Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
*
* @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
* @param array|string $expectedMatches The content of *all* matching tags as an array
* @throws PHPUnit_Framework_AssertionFailedError
* @return boolean
*/
public function assertExactMatchBySelector($selector, $expectedMatches)
{
if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches);
}
$items = $this->cssParser()->getBySelector($selector);
$actuals = array();
if ($items) { if ($items) {
foreach ($items as $item) { foreach ($items as $item) {
$actuals[] = trim(preg_replace("/[ \n\r\t]+/", " ", $item. '')); $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true;
} }
} }
$this->assertTrue( $message = $message ?:
$expectedMatches == $actuals, "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
"Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n"
. implode("'\n'", $expectedMatches) . "'\n\n" . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'";
. "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"
);
return true; foreach ($expectedMatches as $match) {
} $this->assertTrue(isset($actuals[$match]), $message);
}
}
/** /**
* Assert that the most recently queried page contains a number of content tags specified by a CSS selector. * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
* The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag
* will be examined. The assertion fails if one of the expectedMatches fails to appear. * will be examined. The assertion fails if one of the expectedMatches fails to appear.
* *
* Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account. * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
* *
* @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
* @param array|string $expectedMatches The content of at least one of the matched tags * @param array|string $expectedMatches The content of *all* matching tags as an array
* @throws PHPUnit_Framework_AssertionFailedError * @param string $message
* @return boolean * @throws PHPUnit_Framework_AssertionFailedError
*/ */
public function assertPartialHTMLMatchBySelector($selector, $expectedMatches) public function assertExactMatchBySelector($selector, $expectedMatches, $message = null)
{ {
if (is_string($expectedMatches)) { if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches); $expectedMatches = array($expectedMatches);
} }
$items = $this->cssParser()->getBySelector($selector); $items = $this->cssParser()->getBySelector($selector);
$actuals = array(); $actuals = array();
if($items) { if ($items) {
/** @var SimpleXMLElement $item */ foreach ($items as $item) {
foreach($items as $item) { $actuals[] = trim(preg_replace('/\s+/', ' ', (string)$item));
$actuals[$item->asXML()] = true; }
} }
}
foreach($expectedMatches as $match) { $message = $message ?:
$this->assertTrue( "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
isset($actuals[$match]), . implode("'\n'", $expectedMatches) . "'\n\n"
"Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
. implode("'\n'", $expectedMatches) . "'\n\n"
. "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"
);
}
return true; $this->assertTrue($expectedMatches == $actuals, $message);
} }
/** /**
* Assert that the most recently queried page contains a number of content tags specified by a CSS selector. * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
* The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag
* will be examined. The assertion fails if one of the expectedMatches fails to appear. * will be examined. The assertion fails if one of the expectedMatches fails to appear.
* *
* Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account. * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
* *
* @param string $selector A basic CSS selector, e.g. 'li.jobs h3' * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
* @param array|string $expectedMatches The content of *all* matched tags as an array * @param array|string $expectedMatches The content of at least one of the matched tags
* @throws PHPUnit_Framework_AssertionFailedError * @param string $message
*/ * @throws PHPUnit_Framework_AssertionFailedError
public function assertExactHTMLMatchBySelector($selector, $expectedMatches) */
public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null)
{ {
$items = $this->cssParser()->getBySelector($selector); if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches);
}
$actuals = array(); $items = $this->cssParser()->getBySelector($selector);
if($items) {
/** @var SimpleXMLElement $item */
foreach($items as $item) {
$actuals[] = $item->asXML();
}
}
$this->assertTrue( $actuals = array();
$expectedMatches == $actuals, if ($items) {
"Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" /** @var SimpleXMLElement $item */
. implode("'\n'", $expectedMatches) . "'\n\n" foreach ($items as $item) {
. "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'" $actuals[$item->asXML()] = true;
); }
} }
/** $message = $message ?:
* Log in as the given member "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
* . implode("'\n'", $expectedMatches) . "'\n\n"
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'";
*/
foreach ($expectedMatches as $match) {
$this->assertTrue(isset($actuals[$match]), $message);
}
}
/**
* Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
* The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag
* will be examined. The assertion fails if one of the expectedMatches fails to appear.
*
* Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
*
* @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
* @param array|string $expectedMatches The content of *all* matched tags as an array
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError
*/
public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null)
{
$items = $this->cssParser()->getBySelector($selector);
$actuals = array();
if ($items) {
/** @var SimpleXMLElement $item */
foreach ($items as $item) {
$actuals[] = $item->asXML();
}
}
$message = $message ?:
"Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
. implode("'\n'", $expectedMatches) . "'\n\n"
. "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
$this->assertTrue($expectedMatches == $actuals, $message);
}
/**
* Log in as the given member
*
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
*/
public function logInAs($member) public function logInAs($member)
{ {
if (is_object($member)) { if (is_object($member)) {
@ -411,40 +409,40 @@ class FunctionalTest extends SapphireTest
$memberID = $this->idFromFixture('SilverStripe\\Security\\Member', $member); $memberID = $this->idFromFixture('SilverStripe\\Security\\Member', $member);
} }
$this->session()->inst_set('loggedInAs', $memberID); $this->session()->inst_set('loggedInAs', $memberID);
} }
/** /**
* Use the draft (stage) site for testing. * Use the draft (stage) site for testing.
* This is helpful if you're not testing publication functionality and don't want "stage management" cluttering * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering
* your test. * your test.
* *
* @param bool $enabled toggle the use of the draft site * @param bool $enabled toggle the use of the draft site
*/ */
public function useDraftSite($enabled = true) public function useDraftSite($enabled = true)
{ {
if($enabled) { if ($enabled) {
$this->session()->inst_set('readingMode', 'Stage.Stage'); $this->session()->inst_set('readingMode', 'Stage.Stage');
$this->session()->inst_set('unsecuredDraftSite', true); $this->session()->inst_set('unsecuredDraftSite', true);
} else { } else {
$this->session()->inst_set('readingMode', 'Stage.Live'); $this->session()->inst_set('readingMode', 'Stage.Live');
$this->session()->inst_set('unsecuredDraftSite', false); $this->session()->inst_set('unsecuredDraftSite', false);
} }
} }
/** /**
* @return bool * @return bool
*/ */
public static function get_disable_themes() public static function get_disable_themes()
{ {
return static::$disable_themes; return static::$disable_themes;
} }
/** /**
* @return bool * @return bool
*/ */
public static function get_use_draft_site() public static function get_use_draft_site()
{ {
return static::$use_draft_site; return static::$use_draft_site;
} }
} }

View File

@ -519,7 +519,7 @@ class ConfirmedPasswordField extends FormField
// With a valid user and password, check the password is correct // With a valid user and password, check the password is correct
$checkResult = $member->checkPassword($this->currentPasswordValue); $checkResult = $member->checkPassword($this->currentPasswordValue);
if (!$checkResult->valid()) { if (!$checkResult->isValid()) {
$validator->validationError( $validator->validationError(
$name, $name,
_t( _t(

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\ORM\ValidationResult;
/** /**
* Lets you include a nested group of fields inside a template. * Lets you include a nested group of fields inside a template.
@ -147,34 +148,48 @@ class FieldGroup extends CompositeField
/** /**
* @return string * @return string
*/ */
public function Message() public function getMessage()
{ {
$fs = array(); $dataFields = array();
$this->collateDataFields($fs); $this->collateDataFields($dataFields);
foreach ($fs as $subfield) { /** @var FormField $subfield */
if ($m = $subfield->Message()) { $messages = [];
$message[] = rtrim($m, "."); foreach ($dataFields as $subfield) {
$message = $subfield->obj('Message')->forTemplate();
if ($message) {
$messages[] = rtrim($message, ".");
} }
} }
return (isset($message)) ? implode(", ", $message) . "." : ""; if (!$messages) {
return null;
}
return implode(", ", $messages) . ".";
} }
/** /**
* @return string * @return string
*/ */
public function MessageType() public function getMessageType()
{ {
$fs = array(); $dataFields = array();
$this->collateDataFields($fs); $this->collateDataFields($dataFields);
foreach ($fs as $subfield) { /** @var FormField $subfield */
if ($m = $subfield->MessageType()) { foreach ($dataFields as $subfield) {
$MessageType[] = $m; $type = $subfield->getMessageType();
if ($type) {
return $type;
} }
} }
return (isset($MessageType)) ? implode(". ", $MessageType) : ""; return null;
}
public function getMessageCast()
{
return ValidationResult::CAST_HTML;
} }
} }

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use InvalidArgumentException;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
@ -15,10 +14,11 @@ use SilverStripe\Control\HTTP;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\NullSecurityToken; use SilverStripe\Security\NullSecurityToken;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -66,1982 +66,1988 @@ use SilverStripe\View\SSViewer;
*/ */
class Form extends RequestHandler class Form extends RequestHandler
{ {
use FormMessage;
const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded'; /**
const ENC_TYPE_MULTIPART = 'multipart/form-data'; * Form submission data is URL encoded
*/
const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
/** /**
* Accessed by Form.ss; modified by {@link formHtmlContent()}. * Form submission data is multipart form
* A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously */
* const ENC_TYPE_MULTIPART = 'multipart/form-data';
* @var bool
*/
public $IncludeFormTag = true;
/** /**
* @var FieldList * Accessed by Form.ss; modified by {@link formHtmlContent()}.
*/ * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously
protected $fields; *
* @var bool
*/
public $IncludeFormTag = true;
/** /**
* @var FieldList * @var FieldList
*/ */
protected $actions; protected $fields;
/** /**
* @var Controller * @var FieldList
*/ */
protected $controller; protected $actions;
/** /**
* @var string * @var Controller
*/ */
protected $name; protected $controller;
/** /**
* @var Validator * @var string
*/ */
protected $validator; protected $name;
/** /**
* @var callable {@see setValidationResponseCallback()} * @var Validator
*/ */
protected $validationResponseCallback; protected $validator;
/** /**
* @var string * @var callable {@see setValidationResponseCallback()}
*/ */
protected $formMethod = "POST"; protected $validationResponseCallback;
/** /**
* @var boolean * @var string
*/ */
protected $strictFormMethodCheck = false; protected $formMethod = "POST";
/** /**
* @var DataObject|null $record Populated by {@link loadDataFrom()}. * @var boolean
*/ */
protected $record; protected $strictFormMethodCheck = false;
/** /**
* Keeps track of whether this form has a default action or not. * @var DataObject|null $record Populated by {@link loadDataFrom()}.
* Set to false by $this->disableDefaultAction(); */
* protected $record;
* @var boolean
*/
protected $hasDefaultAction = true;
/** /**
* Target attribute of form-tag. * Keeps track of whether this form has a default action or not.
* Useful to open a new window upon * Set to false by $this->disableDefaultAction();
* form submission. *
* * @var boolean
* @var string|null */
*/ protected $hasDefaultAction = true;
protected $target;
/** /**
* Legend value, to be inserted into the * Target attribute of form-tag.
* <legend> element before the <fieldset> * Useful to open a new window upon
* in Form.ss template. * form submission.
* *
* @var string|null * @var string|null
*/ */
protected $legend; protected $target;
/** /**
* The SS template to render this form HTML into. * Legend value, to be inserted into the
* Default is "Form", but this can be changed to * <legend> element before the <fieldset>
* another template for customisation. * in Form.ss template.
* *
* @see Form->setTemplate() * @var string|null
* @var string|null */
*/ protected $legend;
protected $template;
/** /**
* @var callable|null * The SS template to render this form HTML into.
*/ * Default is "Form", but this can be changed to
protected $buttonClickedFunc; * another template for customisation.
*
* @see Form->setTemplate()
* @var string|null
*/
protected $template;
/** /**
* @var string|null * @var callable|null
*/ */
protected $message; protected $buttonClickedFunc;
/** /**
* @var string|null * Should we redirect the user back down to the
*/ * the form on validation errors rather then just the page
protected $messageType; *
* @var bool
*/
protected $redirectToFormOnValidationError = false;
/** /**
* Should we redirect the user back down to the * @var bool
* the form on validation errors rather then just the page */
* protected $security = true;
* @var bool
*/
protected $redirectToFormOnValidationError = false;
/** /**
* @var bool * @var SecurityToken|null
*/ */
protected $security = true; protected $securityToken = null;
/** /**
* @var SecurityToken|null * @var array $extraClasses List of additional CSS classes for the form tag.
*/ */
protected $securityToken = null; protected $extraClasses = array();
/** /**
* @var array $extraClasses List of additional CSS classes for the form tag. * @config
*/ * @var array $default_classes The default classes to apply to the Form
protected $extraClasses = array(); */
private static $default_classes = array();
/** /**
* @config * @var string|null
* @var array $default_classes The default classes to apply to the Form */
*/ protected $encType;
private static $default_classes = array();
/** /**
* @var string|null * @var array Any custom form attributes set through {@link setAttributes()}.
*/ * Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them.
protected $encType; */
protected $attributes = array();
/** /**
* @var array Any custom form attributes set through {@link setAttributes()}. * @var array
* Some attributes are calculated on the fly, so please use {@link getAttributes()} to access them. */
*/ protected $validationExemptActions = array();
protected $attributes = array();
/** private static $allowed_actions = array(
* @var array 'handleField',
*/ 'httpSubmission',
protected $validationExemptActions = array(); 'forTemplate',
);
private static $allowed_actions = array( private static $casting = array(
'handleField', 'AttributesHTML' => 'HTMLFragment',
'httpSubmission', 'FormAttributes' => 'HTMLFragment',
'forTemplate', 'FormName' => 'Text',
); 'Legend' => 'HTMLFragment',
);
private static $casting = array( /**
'AttributesHTML' => 'HTMLFragment', * @var FormTemplateHelper
'FormAttributes' => 'HTMLFragment', */
'MessageType' => 'Text', private $templateHelper = null;
'Message' => 'HTMLFragment',
'FormName' => 'Text',
'Legend' => 'HTMLFragment',
);
/** /**
* @var FormTemplateHelper * @ignore
*/ */
private $templateHelper = null; private $htmlID = null;
/** /**
* @ignore * @ignore
*/ */
private $htmlID = null; private $formActionPath = false;
/** /**
* @ignore * @var bool
*/ */
private $formActionPath = false; protected $securityTokenAdded = false;
/** /**
* @var bool * Create a new form, with the given fields an action buttons.
*/ *
protected $securityTokenAdded = false; * @param Controller $controller The parent controller, necessary to create the appropriate form action tag.
* @param string $name The method on the controller that will return this form object.
/** * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
* Create a new form, with the given fields an action buttons. * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
* * {@link FormAction} objects
* @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
* @param string $name The method on the controller that will return this form object. */
* @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects.
* @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of
* {@link FormAction} objects
* @param Validator|null $validator Override the default validator instance (Default: {@link RequiredFields})
*/
public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null) public function __construct($controller, $name, FieldList $fields, FieldList $actions, Validator $validator = null)
{ {
parent::__construct(); parent::__construct();
$fields->setForm($this); $fields->setForm($this);
$actions->setForm($this); $actions->setForm($this);
$this->fields = $fields; $this->fields = $fields;
$this->actions = $actions; $this->actions = $actions;
$this->controller = $controller; $this->controller = $controller;
$this->setName($name); $this->setName($name);
if (!$this->controller) { if (!$this->controller) {
user_error("$this->class form created without a controller", E_USER_ERROR); user_error("$this->class form created without a controller", E_USER_ERROR);
} }
// Form validation // Form validation
$this->validator = ($validator) ? $validator : new RequiredFields(); $this->validator = ($validator) ? $validator : new RequiredFields();
$this->validator->setForm($this); $this->validator->setForm($this);
// Form error controls // Form error controls
$this->setupFormErrors(); $this->restoreFormState();
// Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that
// method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object. // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object.
if(method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod') if (method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod')
&& $controller->hasMethod('securityTokenEnabled'))) { && $controller->hasMethod('securityTokenEnabled'))) {
$securityEnabled = $controller->securityTokenEnabled(); $securityEnabled = $controller->securityTokenEnabled();
} else { } else {
$securityEnabled = SecurityToken::is_enabled(); $securityEnabled = SecurityToken::is_enabled();
} }
$this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken(); $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken();
$this->setupDefaultClasses(); $this->setupDefaultClasses();
} }
/** /**
* @var array * @var array
*/ */
private static $url_handlers = array( private static $url_handlers = array(
'field/$FieldName!' => 'handleField', 'field/$FieldName!' => 'handleField',
'POST ' => 'httpSubmission', 'POST ' => 'httpSubmission',
'GET ' => 'httpSubmission', 'GET ' => 'httpSubmission',
'HEAD ' => 'httpSubmission', 'HEAD ' => 'httpSubmission',
); );
/** /**
* Take errors from a ValidationResult and populate the form with the appropriate message. * Load form state from session state
* * @return $this
* @param ValidationResult $result The erroneous ValidationResult. If none passed, this will be atken */
* from the session public function restoreFormState()
*/ {
public function setupFormErrors($result = null, $data = null) { // Restore messages
if(!$result) $result = Session::get("FormInfo.{$this->FormName()}.result"); $result = $this->getSessionValidationResult();
if(!$result) return; if (isset($result)) {
$this->loadMessagesFrom($result);
}
foreach($result->fieldErrors() as $fieldName => $fieldError) { // load data in from previous submission upon error
$field = $this->fields->dataFieldByName($fieldName); $data = $this->getSessionData();
$field->setError($fieldError['message'], $fieldError['messageType']); if (isset($data)) {
} $this->loadDataFrom($data);
}
return $this;
}
//don't escape the HTML as it should have been escaped when adding it to the validation result /**
$this->setMessage($result->overallMessage(), $result->valid() ? 'good' : 'bad', false); * Flush persistant form state details
*/
public function clearFormState()
{
Session::clear("FormInfo.{$this->FormName()}.result");
Session::clear("FormInfo.{$this->FormName()}.data");
}
// load data in from previous submission upon error /**
if(!$data) $data = Session::get("FormInfo.{$this->FormName()}.data"); * Return any form data stored in the session
if($data) $this->loadDataFrom($data); *
} * @return array
*/
public function getSessionData()
{
return Session::get("FormInfo.{$this->FormName()}.data");
}
/** /**
* Save information to the session to be picked up by {@link setUpFormErrors()} * Store the given form data in the session
*/ *
public function saveFormErrorsToSession($result = null, $data = null) { * @param array $data
Session::set("FormInfo.{$this->FormName()}.result", $result); */
Session::set("FormInfo.{$this->FormName()}.data", $data); public function setSessionData($data)
} {
Session::set("FormInfo.{$this->FormName()}.data", $data);
}
/** /**
* set up the default classes for the form. This is done on construct so that the default classes can be removed * Return any ValidationResult instance stored for this object
* after instantiation *
*/ * @return ValidationResult The ValidationResult object stored in the session
*/
public function getSessionValidationResult()
{
$resultData = Session::get("FormInfo.{$this->FormName()}.result");
if (isset($resultData)) {
return unserialize($resultData);
}
return null;
}
/**
* Sets the ValidationResult in the session to be used with the next view of this form.
* @param ValidationResult $result The result to save
* @param bool $combineWithExisting If true, then this will be added to the existing result.
*/
public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false)
{
// Combine with existing result
if ($combineWithExisting) {
$existingResult = $this->getSessionValidationResult();
if ($existingResult) {
if ($result) {
$existingResult->combineAnd($result);
} else {
$result = $existingResult;
}
}
}
// Serialise
$resultData = $result ? serialize($result) : null;
Session::set("FormInfo.{$this->FormName()}.result", $resultData);
}
public function clearMessage()
{
$this->setMessage(null);
$this->clearFormState();
}
/**
* Populate this form with messages from the given ValidationResult.
* Note: This will not clear any pre-existing messages
*
* @param ValidationResult $result
* @return $this
*/
public function loadMessagesFrom($result)
{
// Set message on either a field or the parent form
foreach ($result->getMessages() as $message) {
$fieldName = $message['fieldName'];
if ($fieldName) {
$owner = $this->fields->dataFieldByName($fieldName) ?: $this;
} else {
$owner = $this;
}
$owner->setMessage($message['message'], $message['messageType'], $message['messageCast']);
}
return $this;
}
/**
* Set message on a given field name. This message will not persist via redirect.
*
* @param string $fieldName
* @param string $message
* @param string $messageType
* @param string $messageCast
* @return $this
*/
public function setFieldMessage(
$fieldName,
$message,
$messageType = ValidationResult::TYPE_ERROR,
$messageCast = ValidationResult::CAST_TEXT
) {
$field = $this->fields->dataFieldByName($fieldName);
if ($field) {
$field->setMessage($message, $messageType, $messageCast);
}
return $this;
}
public function castingHelper($field)
{
// Override casting for field message
if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
return $helper;
}
return parent::castingHelper($field);
}
/**
* set up the default classes for the form. This is done on construct so that the default classes can be removed
* after instantiation
*/
protected function setupDefaultClasses() protected function setupDefaultClasses()
{ {
$defaultClasses = self::config()->get('default_classes'); $defaultClasses = self::config()->get('default_classes');
if ($defaultClasses) { if ($defaultClasses) {
foreach ($defaultClasses as $class) { foreach ($defaultClasses as $class) {
$this->addExtraClass($class); $this->addExtraClass($class);
} }
} }
} }
/** /**
* Handle a form submission. GET and POST requests behave identically. * Handle a form submission. GET and POST requests behave identically.
* Populates the form with {@link loadDataFrom()}, calls {@link validate()}, * Populates the form with {@link loadDataFrom()}, calls {@link validate()},
* and only triggers the requested form action/method * and only triggers the requested form action/method
* if the form is valid. * if the form is valid.
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @throws HTTPResponse_Exception * @return HTTPResponse
*/ * @throws HTTPResponse_Exception
*/
public function httpSubmission($request) public function httpSubmission($request)
{ {
// Strict method check // Strict method check
if($this->strictFormMethodCheck) { if ($this->strictFormMethodCheck) {
// Throws an error if the method is bad... // Throws an error if the method is bad...
if($this->formMethod != $request->httpMethod()) { if ($this->formMethod != $request->httpMethod()) {
$response = Controller::curr()->getResponse(); $response = Controller::curr()->getResponse();
$response->addHeader('Allow', $this->formMethod); $response->addHeader('Allow', $this->formMethod);
$this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission")); $this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission"));
} }
// ...and only uses the variables corresponding to that method type // ...and only uses the variables corresponding to that method type
$vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars(); $vars = $this->formMethod == 'GET' ? $request->getVars() : $request->postVars();
} else { } else {
$vars = $request->requestVars(); $vars = $request->requestVars();
} }
// Ensure we only process saveable fields (non structural, readonly, or disabled) // Ensure we only process saveable fields (non structural, readonly, or disabled)
$allowedFields = array_keys($this->Fields()->saveableFields()); $allowedFields = array_keys($this->Fields()->saveableFields());
// Populate the form // Populate the form
$this->loadDataFrom($vars, true, $allowedFields); $this->loadDataFrom($vars, true, $allowedFields);
// Protection against CSRF attacks // Protection against CSRF attacks
$token = $this->getSecurityToken(); // @todo Move this to SecurityTokenField::validate()
if( ! $token->checkRequest($request)) { $token = $this->getSecurityToken();
$securityID = $token->getName(); if (! $token->checkRequest($request)) {
if (empty($vars[$securityID])) { $securityID = $token->getName();
if (empty($vars[$securityID])) {
$this->httpError(400, _t( $this->httpError(400, _t(
"Form.CSRF_FAILED_MESSAGE", "Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button, ". "There seems to have been a technical problem. Please click the back button, ".
"refresh your browser, and try again." "refresh your browser, and try again."
)); ));
} else { } else {
// Clear invalid token on refresh // Clear invalid token on refresh
$data = $this->getData(); $this->clearFormState();
unset($data[$securityID]); $data = $this->getData();
Session::set("FormInfo.{$this->FormName()}.data", $data); unset($data[$securityID]);
Session::set("FormInfo.{$this->FormName()}.errors", array()); $this->setSessionData($data);
$this->sessionMessage( $this->sessionError(_t(
_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."), "Form.CSRF_EXPIRED_MESSAGE",
"warning" "Your session has expired. Please re-submit the form."
); ));
return $this->controller->redirectBack();
}
}
// Determine the action button clicked // Return the user
$funcName = null; return $this->controller->redirectBack();
foreach($vars as $paramName => $paramVal) { }
if(substr($paramName,0,7) == 'action_') { }
// Break off querystring arguments included in the action
if(strpos($paramName,'?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array();
parse_str($paramVars, $newRequestParams);
$vars = array_merge((array)$vars, (array)$newRequestParams);
}
// Cleanup action_, _x and _y from image fields // Determine the action button clicked
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName); $funcName = null;
break; foreach ($vars as $paramName => $paramVal) {
} if (substr($paramName, 0, 7) == 'action_') {
} // Break off querystring arguments included in the action
if (strpos($paramName, '?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array();
parse_str($paramVars, $newRequestParams);
$vars = array_merge((array)$vars, (array)$newRequestParams);
}
// If the action wasn't set, choose the default on the form. // Cleanup action_, _x and _y from image fields
if(!isset($funcName) && $defaultAction = $this->defaultAction()){ $funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
$funcName = $defaultAction->actionName(); break;
} }
}
if(isset($funcName)) { // If the action wasn't set, choose the default on the form.
$this->setButtonClicked($funcName); if (!isset($funcName) && $defaultAction = $this->defaultAction()) {
} $funcName = $defaultAction->actionName();
}
// Permission checks (first on controller, then falling back to form) if (isset($funcName)) {
$this->setButtonClicked($funcName);
}
// Permission checks (first on controller, then falling back to form)
if (// Ensure that the action is actually a button or method on the form, if (// Ensure that the action is actually a button or method on the form,
// and not just a method on the controller. // and not just a method on the controller.
$this->controller->hasMethod($funcName) $this->controller->hasMethod($funcName)
&& !$this->controller->checkAccessAction($funcName) && !$this->controller->checkAccessAction($funcName)
// If a button exists, allow it on the controller // If a button exists, allow it on the controller
// buttonClicked() validates that the action set above is valid // buttonClicked() validates that the action set above is valid
&& !$this->buttonClicked() && !$this->buttonClicked()
) { ) {
return $this->httpError( return $this->httpError(
403, 403,
sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller)) sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller))
); );
} elseif ($this->hasMethod($funcName) } elseif ($this->hasMethod($funcName)
&& !$this->checkAccessAction($funcName) && !$this->checkAccessAction($funcName)
// No checks for button existence or $allowed_actions is performed - // No checks for button existence or $allowed_actions is performed -
// all form methods are callable (e.g. the legacy "callfieldmethod()") // all form methods are callable (e.g. the legacy "callfieldmethod()")
) { ) {
return $this->httpError( return $this->httpError(
403, 403,
sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name) sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name)
); );
} }
// TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set
// explicitly in allowed_actions in order to run)
// Uncomment the following for checking security against running actions on form fields
/* else {
// Try to find a field that has the action, and allows it
$fieldsHaveMethod = false;
foreach ($this->Fields() as $field){
if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
$fieldsHaveMethod = true;
}
}
if (!$fieldsHaveMethod) {
return $this->httpError(
403,
sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name())
);
}
}*/
// Action handlers may throw ValidationExceptions. // Action handlers may throw ValidationExceptions.
try { try {
// Or we can use the Valiator attached to the form // Or we can use the Valiator attached to the form
$result = $this->validationResult(); $result = $this->validationResult();
if(!$result->valid()) { if (!$result->isValid()) {
return $this->getValidationErrorResponse($result); return $this->getValidationErrorResponse($result);
} }
// First, try a handler method on the controller (has been checked for allowed_actions above already) // First, try a handler method on the controller (has been checked for allowed_actions above already)
if($this->controller->hasMethod($funcName)) { if ($this->controller->hasMethod($funcName)) {
return $this->controller->$funcName($vars, $this, $request); return $this->controller->$funcName($vars, $this, $request);
// Otherwise, try a handler method on the form object. }
} elseif($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this, $request);
} elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
return $field->$funcName($vars, $this, $request);
}
} catch(ValidationException $e) { // Otherwise, try a handler method on the form object.
// The ValdiationResult contains all the relevant metadata if ($this->hasMethod($funcName)) {
$result = $e->getResult(); return $this->$funcName($vars, $this, $request);
return $this->getValidationErrorResponse($result); }
}
// First, try a handler method on the controller (has been checked for allowed_actions above already) // Check for inline actions
if($this->controller->hasMethod($funcName)) { if ($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
return $this->controller->$funcName($vars, $this, $request); return $field->$funcName($vars, $this, $request);
// Otherwise, try a handler method on the form object. }
} elseif($this->hasMethod($funcName)) { } catch (ValidationException $e) {
return $this->$funcName($vars, $this, $request); // The ValdiationResult contains all the relevant metadata
} elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) { $result = $e->getResult();
return $field->$funcName($vars, $this, $request); $this->loadMessagesFrom($result);
} return $this->getValidationErrorResponse($result);
}
return $this->httpError(404); return $this->httpError(404);
}
/**
* @param string $action
* @return bool
*/
public function checkAccessAction($action)
{
if (parent::checkAccessAction($action)) {
return true;
}
$actions = $this->getAllActions();
foreach ($actions as $formAction) {
if ($formAction->actionName() === $action) {
return true;
}
}
// Always allow actions on fields
$field = $this->checkFieldsForAction($this->Fields(), $action);
if ($field && $field->checkAccessAction($action)) {
return true;
}
return false;
}
/**
* @return callable
*/
public function getValidationResponseCallback()
{
return $this->validationResponseCallback;
}
/**
* Overrules validation error behaviour in {@link httpSubmission()}
* when validation has failed. Useful for optional handling of a certain accepted content type.
*
* The callback can opt out of handling specific responses by returning NULL,
* in which case the default form behaviour will kick in.
*
* @param $callback
* @return self
*/
public function setValidationResponseCallback($callback)
{
$this->validationResponseCallback = $callback;
return $this;
}
/**
* Returns the appropriate response up the controller chain
* if {@link validate()} fails (which is checked prior to executing any form actions).
* By default, returns different views for ajax/non-ajax request, and
* handles 'application/json' requests with a JSON object containing the error messages.
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
* and can be overruled by setting {@link $validationResponseCallback}.
*
* @param ValidationResult $result
* @return HTTPResponse|string
*/
protected function getValidationErrorResponse(ValidationResult $result) {
$callback = $this->getValidationResponseCallback();
if($callback && $callbackResponse = $callback($result)) {
return $callbackResponse;
}
$request = $this->getRequest();
if($request->isAjax()) {
// Special case for legacy Validator.js implementation
// (assumes eval'ed javascript collected through FormResponse)
$acceptType = $request->getHeader('Accept');
if (strpos($acceptType, 'application/json') !== false) {
// Send validation errors back as JSON with a flag at the start
$response = new HTTPResponse(Convert::array2json($result->getErrorMetaData()));
$response->addHeader('Content-Type', 'application/json');
} else {
$this->setupFormErrors($result, $this->getData());
// Send the newly rendered form tag as HTML
$response = new HTTPResponse($this->forTemplate());
$response->addHeader('Content-Type', 'text/html');
}
return $response;
} else {
// Save the relevant information in the session
$this->saveFormErrorsToSession($result, $this->getData());
// Redirect back to the form
if($this->getRedirectToFormOnValidationError()) {
if($pageURL = $request->getHeader('Referer')) {
if(Director::is_site_url($pageURL)) {
// Remove existing pragmas
$pageURL = preg_replace('/(#.*)/', '', $pageURL);
$pageURL = Director::absoluteURL($pageURL, true);
return $this->controller->redirect($pageURL . '#' . $this->FormName());
}
}
}
return $this->controller->redirectBack();
}
}
/**
* Fields can have action to, let's check if anyone of the responds to $funcname them
*
* @param SS_List|array $fields
* @param callable $funcName
* @return FormField
*/
protected function checkFieldsForAction($fields, $funcName)
{
foreach($fields as $field){
/** @skipUpgrade */
if(method_exists($field, 'FieldList')) {
if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
return $field;
}
} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
return $field;
}
}
return null;
}
/**
* Handle a field request.
* Uses {@link Form->dataFieldByName()} to find a matching field,
* and falls back to {@link FieldList->fieldByName()} to look
* for tabs instead. This means that if you have a tab and a
* formfield with the same name, this method gives priority
* to the formfield.
*
* @param HTTPRequest $request
* @return FormField
*/
public function handleField($request)
{
$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
if($field) {
return $field;
} else {
// falling back to fieldByName, e.g. for getting tabs
return $this->Fields()->fieldByName($request->param('FieldName'));
}
}
/**
* Convert this form into a readonly form
*/
public function makeReadonly()
{
$this->transform(new ReadonlyTransformation());
}
/**
* Set whether the user should be redirected back down to the
* form on the page upon validation errors in the form or if
* they just need to redirect back to the page
*
* @param bool $bool Redirect to form on error?
* @return $this
*/
public function setRedirectToFormOnValidationError($bool)
{
$this->redirectToFormOnValidationError = $bool;
return $this;
}
/**
* Get whether the user should be redirected back down to the
* form on the page upon validation errors
*
* @return bool
*/
public function getRedirectToFormOnValidationError()
{
return $this->redirectToFormOnValidationError;
}
/**
* Add a plain text error message to a field on this form. It will be saved into the session
* and used the next time this form is displayed.
*
* @deprecated 3.2
*/
public function addErrorMessage($fieldName, $message, $messageType) {
Deprecation::notice('3.2', 'Throw a ValidationException instead.');
$this->getSessionValidationResult()->addFieldError($fieldName, $message, $messageType);
}
/**
* @param FormTransformation $trans
*/
public function transform(FormTransformation $trans)
{
$newFields = new FieldList();
foreach($this->fields as $field) {
$newFields->push($field->transform($trans));
}
$this->fields = $newFields;
$newActions = new FieldList();
foreach($this->actions as $action) {
$newActions->push($action->transform($trans));
}
$this->actions = $newActions;
// We have to remove validation, if the fields are not editable ;-)
if ($this->validator) {
$this->validator->removeValidation();
}
} }
/** /**
* Get the {@link Validator} attached to this form. * @param string $action
* @return Validator * @return bool
*/ */
public function checkAccessAction($action)
{
if (parent::checkAccessAction($action)) {
return true;
}
$actions = $this->getAllActions();
foreach ($actions as $formAction) {
if ($formAction->actionName() === $action) {
return true;
}
}
// Always allow actions on fields
$field = $this->checkFieldsForAction($this->Fields(), $action);
if ($field && $field->checkAccessAction($action)) {
return true;
}
return false;
}
/**
* @return callable
*/
public function getValidationResponseCallback()
{
return $this->validationResponseCallback;
}
/**
* Overrules validation error behaviour in {@link httpSubmission()}
* when validation has failed. Useful for optional handling of a certain accepted content type.
*
* The callback can opt out of handling specific responses by returning NULL,
* in which case the default form behaviour will kick in.
*
* @param $callback
* @return self
*/
public function setValidationResponseCallback($callback)
{
$this->validationResponseCallback = $callback;
return $this;
}
/**
* Returns the appropriate response up the controller chain
* if {@link validate()} fails (which is checked prior to executing any form actions).
* By default, returns different views for ajax/non-ajax request, and
* handles 'application/json' requests with a JSON object containing the error messages.
* Behaviour can be influenced by setting {@link $redirectToFormOnValidationError},
* and can be overruled by setting {@link $validationResponseCallback}.
*
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getValidationErrorResponse(ValidationResult $result)
{
// Check for custom handling mechanism
$callback = $this->getValidationResponseCallback();
if ($callback && $callbackResponse = call_user_func($callback, $result)) {
return $callbackResponse;
}
// Check if handling via ajax
if ($this->getRequest()->isAjax()) {
return $this->getAjaxErrorResponse($result);
}
// Prior to redirection, persist this result in session to re-display on redirect
$this->setSessionValidationResult($result);
$this->setSessionData($this->getData());
// Determine redirection method
if ($this->getRedirectToFormOnValidationError() && ($pageURL = $this->getRedirectReferer())) {
return $this->controller->redirect($pageURL . '#' . $this->FormName());
}
return $this->controller->redirectBack();
}
/**
* Build HTTP error response for ajax requests
*
* @internal called from {@see Form::getValidationErrorResponse}
* @param ValidationResult $result
* @return HTTPResponse
*/
protected function getAjaxErrorResponse(ValidationResult $result)
{
// Ajax form submissions accept json encoded errors by default
$acceptType = $this->getRequest()->getHeader('Accept');
if (strpos($acceptType, 'application/json') !== false) {
// Send validation errors back as JSON with a flag at the start
$response = new HTTPResponse(Convert::array2json($result->getMessages()));
$response->addHeader('Content-Type', 'application/json');
return $response;
}
// Send the newly rendered form tag as HTML
$this->loadMessagesFrom($result);
$response = new HTTPResponse($this->forTemplate());
$response->addHeader('Content-Type', 'text/html');
return $response;
}
/**
* Get referrer to redirect back to and safely validates it
*
* @internal called from {@see Form::getValidationErrorResponse}
* @return string|null
*/
protected function getRedirectReferer()
{
$pageURL = $this->getRequest()->getHeader('Referer');
if (!$pageURL) {
return null;
}
if (!Director::is_site_url($pageURL)) {
return null;
}
// Remove existing pragmas
$pageURL = preg_replace('/(#.*)/', '', $pageURL);
return Director::absoluteURL($pageURL);
}
/**
* Fields can have action to, let's check if anyone of the responds to $funcname them
*
* @param SS_List|array $fields
* @param callable $funcName
* @return FormField
*/
protected function checkFieldsForAction($fields, $funcName)
{
foreach ($fields as $field) {
/** @skipUpgrade */
if (method_exists($field, 'FieldList')) {
if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
return $field;
}
} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
return $field;
}
}
return null;
}
/**
* Handle a field request.
* Uses {@link Form->dataFieldByName()} to find a matching field,
* and falls back to {@link FieldList->fieldByName()} to look
* for tabs instead. This means that if you have a tab and a
* formfield with the same name, this method gives priority
* to the formfield.
*
* @param HTTPRequest $request
* @return FormField
*/
public function handleField($request)
{
$field = $this->Fields()->dataFieldByName($request->param('FieldName'));
if ($field) {
return $field;
} else {
// falling back to fieldByName, e.g. for getting tabs
return $this->Fields()->fieldByName($request->param('FieldName'));
}
}
/**
* Convert this form into a readonly form
*/
public function makeReadonly()
{
$this->transform(new ReadonlyTransformation());
}
/**
* Set whether the user should be redirected back down to the
* form on the page upon validation errors in the form or if
* they just need to redirect back to the page
*
* @param bool $bool Redirect to form on error?
* @return $this
*/
public function setRedirectToFormOnValidationError($bool)
{
$this->redirectToFormOnValidationError = $bool;
return $this;
}
/**
* Get whether the user should be redirected back down to the
* form on the page upon validation errors
*
* @return bool
*/
public function getRedirectToFormOnValidationError()
{
return $this->redirectToFormOnValidationError;
}
/**
* @param FormTransformation $trans
*/
public function transform(FormTransformation $trans)
{
$newFields = new FieldList();
foreach ($this->fields as $field) {
$newFields->push($field->transform($trans));
}
$this->fields = $newFields;
$newActions = new FieldList();
foreach ($this->actions as $action) {
$newActions->push($action->transform($trans));
}
$this->actions = $newActions;
// We have to remove validation, if the fields are not editable ;-)
if ($this->validator) {
$this->validator->removeValidation();
}
}
/**
* Get the {@link Validator} attached to this form.
* @return Validator
*/
public function getValidator() public function getValidator()
{ {
return $this->validator; return $this->validator;
} }
/** /**
* Set the {@link Validator} on this form. * Set the {@link Validator} on this form.
* @param Validator $validator * @param Validator $validator
* @return $this * @return $this
*/ */
public function setValidator(Validator $validator) public function setValidator(Validator $validator)
{ {
if($validator) { if ($validator) {
$this->validator = $validator; $this->validator = $validator;
$this->validator->setForm($this); $this->validator->setForm($this);
} }
return $this; return $this;
} }
/** /**
* Remove the {@link Validator} from this from. * Remove the {@link Validator} from this from.
*/ */
public function unsetValidator() public function unsetValidator()
{ {
$this->validator = null; $this->validator = null;
return $this; return $this;
} }
/** /**
* Set actions that are exempt from validation * Set actions that are exempt from validation
* *
* @param array * @param array
* @return $this * @return $this
*/ */
public function setValidationExemptActions($actions) public function setValidationExemptActions($actions)
{ {
$this->validationExemptActions = $actions; $this->validationExemptActions = $actions;
return $this; return $this;
} }
/** /**
* Get a list of actions that are exempt from validation * Get a list of actions that are exempt from validation
* *
* @return array * @return array
*/ */
public function getValidationExemptActions() public function getValidationExemptActions()
{ {
return $this->validationExemptActions; return $this->validationExemptActions;
} }
/** /**
* Passed a FormAction, returns true if that action is exempt from Form validation * Passed a FormAction, returns true if that action is exempt from Form validation
* *
* @param FormAction $action * @param FormAction $action
* @return bool * @return bool
*/ */
public function actionIsValidationExempt($action) public function actionIsValidationExempt($action)
{ {
if ($action->getValidationExempt()) { if ($action->getValidationExempt()) {
return true; return true;
} }
if (in_array($action->actionName(), $this->getValidationExemptActions())) { if (in_array($action->actionName(), $this->getValidationExemptActions())) {
return true; return true;
} }
return false; return false;
} }
/** /**
* Generate extra special fields - namely the security token field (if required). * Generate extra special fields - namely the security token field (if required).
* *
* @return FieldList * @return FieldList
*/ */
public function getExtraFields() public function getExtraFields()
{ {
$extraFields = new FieldList(); $extraFields = new FieldList();
$token = $this->getSecurityToken(); $token = $this->getSecurityToken();
if ($token) { if ($token) {
$tokenField = $token->updateFieldSet($this->fields); $tokenField = $token->updateFieldSet($this->fields);
if ($tokenField) { if ($tokenField) {
$tokenField->setForm($this); $tokenField->setForm($this);
} }
} }
$this->securityTokenAdded = true; $this->securityTokenAdded = true;
// add the "real" HTTP method if necessary (for PUT, DELETE and HEAD) // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD)
if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) { if (strtoupper($this->FormMethod()) != $this->FormHttpMethod()) {
$methodField = new HiddenField('_method', '', $this->FormHttpMethod()); $methodField = new HiddenField('_method', '', $this->FormHttpMethod());
$methodField->setForm($this); $methodField->setForm($this);
$extraFields->push($methodField); $extraFields->push($methodField);
} }
return $extraFields; return $extraFields;
} }
/** /**
* Return the form's fields - used by the templates * Return the form's fields - used by the templates
* *
* @return FieldList The form fields * @return FieldList The form fields
*/ */
public function Fields() public function Fields()
{ {
foreach($this->getExtraFields() as $field) { foreach ($this->getExtraFields() as $field) {
if (!$this->fields->fieldByName($field->getName())) { if (!$this->fields->fieldByName($field->getName())) {
$this->fields->push($field); $this->fields->push($field);
} }
} }
return $this->fields; return $this->fields;
} }
/** /**
* Return all <input type="hidden"> fields * Return all <input type="hidden"> fields
* in a form - including fields nested in {@link CompositeFields}. * in a form - including fields nested in {@link CompositeFields}.
* Useful when doing custom field layouts. * Useful when doing custom field layouts.
* *
* @return FieldList * @return FieldList
*/ */
public function HiddenFields() public function HiddenFields()
{ {
return $this->Fields()->HiddenFields(); return $this->Fields()->HiddenFields();
} }
/** /**
* Return all fields except for the hidden fields. * Return all fields except for the hidden fields.
* Useful when making your own simplified form layouts. * Useful when making your own simplified form layouts.
*/ */
public function VisibleFields() public function VisibleFields()
{ {
return $this->Fields()->VisibleFields(); return $this->Fields()->VisibleFields();
} }
/** /**
* Setter for the form fields. * Setter for the form fields.
* *
* @param FieldList $fields * @param FieldList $fields
* @return $this * @return $this
*/ */
public function setFields($fields) public function setFields($fields)
{ {
$this->fields = $fields; $this->fields = $fields;
return $this; return $this;
} }
/** /**
* Return the form's action buttons - used by the templates * Return the form's action buttons - used by the templates
* *
* @return FieldList The action list * @return FieldList The action list
*/ */
public function Actions() public function Actions()
{ {
return $this->actions; return $this->actions;
} }
/** /**
* Setter for the form actions. * Setter for the form actions.
* *
* @param FieldList $actions * @param FieldList $actions
* @return $this * @return $this
*/ */
public function setActions($actions) public function setActions($actions)
{ {
$this->actions = $actions; $this->actions = $actions;
return $this; return $this;
} }
/** /**
* Unset all form actions * Unset all form actions
*/ */
public function unsetAllActions() public function unsetAllActions()
{ {
$this->actions = new FieldList(); $this->actions = new FieldList();
return $this; return $this;
} }
/** /**
* @param string $name * @param string $name
* @param string $value * @param string $value
* @return $this * @return $this
*/ */
public function setAttribute($name, $value) public function setAttribute($name, $value)
{ {
$this->attributes[$name] = $value; $this->attributes[$name] = $value;
return $this; return $this;
} }
/** /**
* @param string $name * @param string $name
* @return string * @return string
*/ */
public function getAttribute($name) public function getAttribute($name)
{ {
if(isset($this->attributes[$name])) { if (isset($this->attributes[$name])) {
return $this->attributes[$name]; return $this->attributes[$name];
} }
return null; return null;
} }
/** /**
* @return array * @return array
*/ */
public function getAttributes() public function getAttributes()
{ {
$attrs = array( $attrs = array(
'id' => $this->FormName(), 'id' => $this->FormName(),
'action' => $this->FormAction(), 'action' => $this->FormAction(),
'method' => $this->FormMethod(), 'method' => $this->FormMethod(),
'enctype' => $this->getEncType(), 'enctype' => $this->getEncType(),
'target' => $this->target, 'target' => $this->target,
'class' => $this->extraClass(), 'class' => $this->extraClass(),
); );
if($this->validator && $this->validator->getErrors()) { if ($this->validator && $this->validator->getErrors()) {
if (!isset($attrs['class'])) { if (!isset($attrs['class'])) {
$attrs['class'] = ''; $attrs['class'] = '';
} }
$attrs['class'] .= ' validationerror'; $attrs['class'] .= ' validationerror';
} }
$attrs = array_merge($attrs, $this->attributes); $attrs = array_merge($attrs, $this->attributes);
return $attrs; return $attrs;
} }
/** /**
* Return the attributes of the form tag - used by the templates. * Return the attributes of the form tag - used by the templates.
* *
* @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}. * @param array $attrs Custom attributes to process. Falls back to {@link getAttributes()}.
* If at least one argument is passed as a string, all arguments act as excludes by name. * If at least one argument is passed as a string, all arguments act as excludes by name.
* *
* @return string HTML attributes, ready for insertion into an HTML tag * @return string HTML attributes, ready for insertion into an HTML tag
*/ */
public function getAttributesHTML($attrs = null) public function getAttributesHTML($attrs = null)
{ {
$exclude = (is_string($attrs)) ? func_get_args() : null; $exclude = (is_string($attrs)) ? func_get_args() : null;
// Figure out if we can cache this form // Figure out if we can cache this form
// - forms with validation shouldn't be cached, cos their error messages won't be shown // - forms with validation shouldn't be cached, cos their error messages won't be shown
// - forms with security tokens shouldn't be cached because security tokens expire // - forms with security tokens shouldn't be cached because security tokens expire
$needsCacheDisabled = false; $needsCacheDisabled = false;
if ($this->getSecurityToken()->isEnabled()) { if ($this->getSecurityToken()->isEnabled()) {
$needsCacheDisabled = true; $needsCacheDisabled = true;
} }
if ($this->FormMethod() != 'GET') { if ($this->FormMethod() != 'GET') {
$needsCacheDisabled = true; $needsCacheDisabled = true;
} }
if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) { if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
$needsCacheDisabled = true; $needsCacheDisabled = true;
} }
// If we need to disable cache, do it // If we need to disable cache, do it
if ($needsCacheDisabled) { if ($needsCacheDisabled) {
HTTP::set_cache_age(0); HTTP::set_cache_age(0);
} }
$attrs = $this->getAttributes(); $attrs = $this->getAttributes();
// Remove empty // Remove empty
$attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);')); $attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);'));
// Remove excluded // Remove excluded
if ($exclude) { if ($exclude) {
$attrs = array_diff_key($attrs, array_flip($exclude)); $attrs = array_diff_key($attrs, array_flip($exclude));
} }
// Prepare HTML-friendly 'method' attribute (lower-case) // Prepare HTML-friendly 'method' attribute (lower-case)
if (isset($attrs['method'])) { if (isset($attrs['method'])) {
$attrs['method'] = strtolower($attrs['method']); $attrs['method'] = strtolower($attrs['method']);
} }
// Create markup // Create markup
$parts = array(); $parts = array();
foreach($attrs as $name => $value) { foreach ($attrs as $name => $value) {
$parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\""; $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\"";
} }
return implode(' ', $parts); return implode(' ', $parts);
} }
public function FormAttributes() public function FormAttributes()
{ {
return $this->getAttributesHTML(); return $this->getAttributesHTML();
} }
/** /**
* Set the target of this form to any value - useful for opening the form contents in a new window or refreshing * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing
* another frame * another frame
* *
* @param string|FormTemplateHelper * @param string|FormTemplateHelper
*/ */
public function setTemplateHelper($helper) public function setTemplateHelper($helper)
{ {
$this->templateHelper = $helper; $this->templateHelper = $helper;
} }
/** /**
* Return a {@link FormTemplateHelper} for this form. If one has not been * Return a {@link FormTemplateHelper} for this form. If one has not been
* set, return the default helper. * set, return the default helper.
* *
* @return FormTemplateHelper * @return FormTemplateHelper
*/ */
public function getTemplateHelper() public function getTemplateHelper()
{ {
if($this->templateHelper) { if ($this->templateHelper) {
if(is_string($this->templateHelper)) { if (is_string($this->templateHelper)) {
return Injector::inst()->get($this->templateHelper); return Injector::inst()->get($this->templateHelper);
} }
return $this->templateHelper; return $this->templateHelper;
} }
return FormTemplateHelper::singleton(); return FormTemplateHelper::singleton();
} }
/** /**
* Set the target of this form to any value - useful for opening the form * Set the target of this form to any value - useful for opening the form
* contents in a new window or refreshing another frame. * contents in a new window or refreshing another frame.
* *
* @param string $target The value of the target * @param string $target The value of the target
* @return $this * @return $this
*/ */
public function setTarget($target) public function setTarget($target)
{ {
$this->target = $target; $this->target = $target;
return $this; return $this;
} }
/** /**
* Set the legend value to be inserted into * Set the legend value to be inserted into
* the <legend> element in the Form.ss template. * the <legend> element in the Form.ss template.
* @param string $legend * @param string $legend
* @return $this * @return $this
*/ */
public function setLegend($legend) public function setLegend($legend)
{ {
$this->legend = $legend; $this->legend = $legend;
return $this; return $this;
} }
/** /**
* Set the SS template that this form should use * Set the SS template that this form should use
* to render with. The default is "Form". * to render with. The default is "Form".
* *
* @param string $template The name of the template (without the .ss extension) * @param string $template The name of the template (without the .ss extension)
* @return $this * @return $this
*/ */
public function setTemplate($template) public function setTemplate($template)
{ {
$this->template = $template; $this->template = $template;
return $this; return $this;
} }
/** /**
* Return the template to render this form with. * Return the template to render this form with.
* *
* @return string * @return string
*/ */
public function getTemplate() public function getTemplate()
{ {
return $this->template; return $this->template;
} }
/** /**
* Returs the ordered list of preferred templates for rendering this form * Returs the ordered list of preferred templates for rendering this form
* If the template isn't set, then default to the * If the template isn't set, then default to the
* form class name e.g "Form". * form class name e.g "Form".
* *
* @return array * @return array
*/ */
public function getTemplates() public function getTemplates()
{ {
$templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__); $templates = SSViewer::get_templates_by_class(get_class($this), '', __CLASS__);
// Prefer any custom template // Prefer any custom template
if($this->getTemplate()) { if ($this->getTemplate()) {
array_unshift($templates, $this->getTemplate()); array_unshift($templates, $this->getTemplate());
} }
return $templates; return $templates;
} }
/** /**
* Returns the encoding type for the form. * Returns the encoding type for the form.
* *
* By default this will be URL encoded, unless there is a file field present * By default this will be URL encoded, unless there is a file field present
* in which case multipart is used. You can also set the enc type using * in which case multipart is used. You can also set the enc type using
* {@link setEncType}. * {@link setEncType}.
*/ */
public function getEncType() public function getEncType()
{ {
if ($this->encType) { if ($this->encType) {
return $this->encType; return $this->encType;
} }
if ($fields = $this->fields->dataFields()) { if ($fields = $this->fields->dataFields()) {
foreach ($fields as $field) { foreach ($fields as $field) {
if ($field instanceof FileField) { if ($field instanceof FileField) {
return self::ENC_TYPE_MULTIPART; return self::ENC_TYPE_MULTIPART;
} }
} }
} }
return self::ENC_TYPE_URLENCODED; return self::ENC_TYPE_URLENCODED;
} }
/** /**
* Sets the form encoding type. The most common encoding types are defined * Sets the form encoding type. The most common encoding types are defined
* in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}. * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}.
* *
* @param string $encType * @param string $encType
* @return $this * @return $this
*/ */
public function setEncType($encType) public function setEncType($encType)
{ {
$this->encType = $encType; $this->encType = $encType;
return $this; return $this;
} }
/** /**
* Returns the real HTTP method for the form: * Returns the real HTTP method for the form:
* GET, POST, PUT, DELETE or HEAD. * GET, POST, PUT, DELETE or HEAD.
* As most browsers only support GET and POST in * As most browsers only support GET and POST in
* form submissions, all other HTTP methods are * form submissions, all other HTTP methods are
* added as a hidden field "_method" that * added as a hidden field "_method" that
* gets evaluated in {@link Director::direct()}. * gets evaluated in {@link Director::direct()}.
* See {@link FormMethod()} to get a HTTP method * See {@link FormMethod()} to get a HTTP method
* for safe insertion into a <form> tag. * for safe insertion into a <form> tag.
* *
* @return string HTTP method * @return string HTTP method
*/ */
public function FormHttpMethod() public function FormHttpMethod()
{ {
return $this->formMethod; return $this->formMethod;
} }
/** /**
* Returns the form method to be used in the <form> tag. * Returns the form method to be used in the <form> tag.
* See {@link FormHttpMethod()} to get the "real" method. * See {@link FormHttpMethod()} to get the "real" method.
* *
* @return string Form HTTP method restricted to 'GET' or 'POST' * @return string Form HTTP method restricted to 'GET' or 'POST'
*/ */
public function FormMethod() public function FormMethod()
{ {
if(in_array($this->formMethod,array('GET','POST'))) { if (in_array($this->formMethod, array('GET','POST'))) {
return $this->formMethod; return $this->formMethod;
} else { } else {
return 'POST'; return 'POST';
} }
} }
/** /**
* Set the form method: GET, POST, PUT, DELETE. * Set the form method: GET, POST, PUT, DELETE.
* *
* @param string $method * @param string $method
* @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}.
* @return $this * @return $this
*/ */
public function setFormMethod($method, $strict = null) public function setFormMethod($method, $strict = null)
{ {
$this->formMethod = strtoupper($method); $this->formMethod = strtoupper($method);
if ($strict !== null) { if ($strict !== null) {
$this->setStrictFormMethodCheck($strict); $this->setStrictFormMethodCheck($strict);
} }
return $this; return $this;
} }
/** /**
* If set to true, enforce the matching of the form method. * If set to true, enforce the matching of the form method.
* *
* This will mean two things: * This will mean two things:
* - GET vars will be ignored by a POST form, and vice versa * - GET vars will be ignored by a POST form, and vice versa
* - A submission where the HTTP method used doesn't match the form will return a 400 error. * - A submission where the HTTP method used doesn't match the form will return a 400 error.
* *
* If set to false (the default), then the form method is only used to construct the default * If set to false (the default), then the form method is only used to construct the default
* form. * form.
* *
* @param $bool boolean * @param $bool boolean
* @return $this * @return $this
*/ */
public function setStrictFormMethodCheck($bool) public function setStrictFormMethodCheck($bool)
{ {
$this->strictFormMethodCheck = (bool)$bool; $this->strictFormMethodCheck = (bool)$bool;
return $this; return $this;
} }
/** /**
* @return boolean * @return boolean
*/ */
public function getStrictFormMethodCheck() public function getStrictFormMethodCheck()
{ {
return $this->strictFormMethodCheck; return $this->strictFormMethodCheck;
} }
/** /**
* Return the form's action attribute. * Return the form's action attribute.
* This is build by adding an executeForm get variable to the parent controller's Link() value * This is build by adding an executeForm get variable to the parent controller's Link() value
* *
* @return string * @return string
*/ */
public function FormAction() public function FormAction()
{ {
if ($this->formActionPath) { if ($this->formActionPath) {
return $this->formActionPath; return $this->formActionPath;
} elseif($this->controller->hasMethod("FormObjectLink")) { } elseif ($this->controller->hasMethod("FormObjectLink")) {
return $this->controller->FormObjectLink($this->name); return $this->controller->FormObjectLink($this->name);
} else { } else {
return Controller::join_links($this->controller->Link(), $this->name); return Controller::join_links($this->controller->Link(), $this->name);
} }
} }
/** /**
* Set the form action attribute to a custom URL. * Set the form action attribute to a custom URL.
* *
* Note: For "normal" forms, you shouldn't need to use this method. It is * Note: For "normal" forms, you shouldn't need to use this method. It is
* recommended only for situations where you have two relatively distinct * recommended only for situations where you have two relatively distinct
* parts of the system trying to communicate via a form post. * parts of the system trying to communicate via a form post.
* *
* @param string $path * @param string $path
* @return $this * @return $this
*/ */
public function setFormAction($path) public function setFormAction($path)
{ {
$this->formActionPath = $path; $this->formActionPath = $path;
return $this; return $this;
} }
/** /**
* Returns the name of the form. * Returns the name of the form.
* *
* @return string * @return string
*/ */
public function FormName() public function FormName()
{ {
return $this->getTemplateHelper()->generateFormID($this); return $this->getTemplateHelper()->generateFormID($this);
} }
/** /**
* Set the HTML ID attribute of the form. * Set the HTML ID attribute of the form.
* *
* @param string $id * @param string $id
* @return $this * @return $this
*/ */
public function setHTMLID($id) public function setHTMLID($id)
{ {
$this->htmlID = $id; $this->htmlID = $id;
return $this; return $this;
} }
/** /**
* @return string * @return string
*/ */
public function getHTMLID() public function getHTMLID()
{ {
return $this->htmlID; return $this->htmlID;
} }
/** /**
* Get the controller. * Get the controller.
* *
* @return Controller * @return Controller
*/ */
public function getController() public function getController()
{ {
return $this->controller; return $this->controller;
} }
/** /**
* Set the controller. * Set the controller.
* *
* @param Controller $controller * @param Controller $controller
* @return Form * @return Form
*/ */
public function setController($controller) public function setController($controller)
{ {
$this->controller = $controller; $this->controller = $controller;
return $this; return $this;
} }
/** /**
* Get the name of the form. * Get the name of the form.
* *
* @return string * @return string
*/ */
public function getName() public function getName()
{ {
return $this->name; return $this->name;
} }
/** /**
* Set the name of the form. * Set the name of the form.
* *
* @param string $name * @param string $name
* @return Form * @return Form
*/ */
public function setName($name) public function setName($name)
{ {
$this->name = $name; $this->name = $name;
return $this; return $this;
} }
/** /**
* Returns an object where there is a method with the same name as each data * Returns an object where there is a method with the same name as each data
* field on the form. * field on the form.
* *
* That method will return the field itself. * That method will return the field itself.
* *
* It means that you can execute $firstName = $form->FieldMap()->FirstName() * It means that you can execute $firstName = $form->FieldMap()->FirstName()
*/ */
public function FieldMap() public function FieldMap()
{ {
return new Form_FieldMap($this); return new Form_FieldMap($this);
} }
/** /**
* The next functions store and modify the forms * Set a message to the session, for display next time this form is shown.
* message attributes. messages are stored in session under *
* $_SESSION[formname][message]; * @param string $message the text of the message
* * @param string $type Should be set to good, bad, or warning.
* @return string * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
*/ * Bool values will be treated as plain text flag.
public function Message() { */
return $this->message; public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
}
/**
* @return string
*/
public function MessageType() {
return $this->messageType;
}
/**
* Set a status message for the form.
*
* @param string $message the text of the message
* @param string $type Should be set to good, bad, or warning.
* @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
* @return $this
*/
public function setMessage($message, $type, $escapeHtml = true)
{ {
$this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message; $this->setMessage($message, $type, $cast);
$this->messageType = $type; $result = $this->getSessionValidationResult() ?: ValidationResult::create();
return $this; $result->addMessage($message, $type, null, $cast);
} $this->setSessionValidationResult($result);
}
/** /**
* Set a message to the session, for display next time this form is shown. * Set an error to the session, for display next time this form is shown.
* *
* @param string $message the text of the message * @param string $message the text of the message
* @param string $type Should be set to good, bad, or warning. * @param string $type Should be set to good, bad, or warning.
* @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML. * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any * Bool values will be treated as plain text flag.
* user supplied data in the message. */
*/ public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
public function sessionMessage($message, $type, $escapeHtml = true)
{ {
// Benign message $this->setMessage($message, $type, $cast);
if($type == "good") { $result = $this->getSessionValidationResult() ?: ValidationResult::create();
$this->getSessionValidationResult()->addMessage($message, $type, null, $escapeHtml); $result->addError($message, $type, null, $cast);
$this->setSessionValidationResult($result);
}
// Bad message causing a validation error /**
} else { * Returns the DataObject that has given this form its data
$this->getSessionValidationResult()->addError($message, $type, null, $escapeHtml * through {@link loadDataFrom()}.
); *
} * @return DataObject
} */
/**
* @deprecated 3.1
*/
public static function messageForForm($formName, $message, $type) {
Deprecation::notice('3.1', 'Create an instance of the form you wish to attach a message to.');
}
/**
* Returns the ValidationResult stored in the session.
* You can use this to modify messages without throwing a ValidationException.
* If a ValidationResult doesn't yet exist, a new one will be created
*
* @return ValidationResult The ValidationResult object stored in the session
*/
public function getSessionValidationResult() {
$result = Session::get("FormInfo.{$this->FormName()}.result");
if(!$result || !($result instanceof ValidationResult)) {
$result = new ValidationResult;
Session::set("FormInfo.{$this->FormName()}.result", $result);
}
return $result;
}
/**
* Sets the ValidationResult in the session to be used with the next view of this form.
* @param ValidationResult $result The result to save
* @param boolean $combineWithExisting If true, then this will be added to the existing result.
*/
public function setSessionValidationResult(ValidationResult $result, $combineWithExisting = false) {
if($combineWithExisting) {
$existingResult = $this->getSessionValidationResult();
$existingResult->combineAnd($result);
} else {
Session::set("FormInfo.{$this->FormName()}.result", $result);
}
}
public function clearMessage()
{
$this->message = null;
Session::clear("FormInfo.{$this->FormName()}.result");
Session::clear("FormInfo.{$this->FormName()}.data");
}
public function resetValidation() {
Session::clear("FormInfo.{$this->FormName()}.data");
Session::clear("FormInfo.{$this->FormName()}.result");
}
/**
* Returns the DataObject that has given this form its data
* through {@link loadDataFrom()}.
*
* @return DataObject
*/
public function getRecord() public function getRecord()
{ {
return $this->record; return $this->record;
} }
/** /**
* Get the legend value to be inserted into the * Get the legend value to be inserted into the
* <legend> element in Form.ss * <legend> element in Form.ss
* *
* @return string * @return string
*/ */
public function getLegend() public function getLegend()
{ {
return $this->legend; return $this->legend;
} }
/** /**
* Processing that occurs before a form is executed. * Processing that occurs before a form is executed.
* *
* This includes form validation, if it fails, we throw a ValidationException * This includes form validation, if it fails, we throw a ValidationException
* *
* This includes form validation, if it fails, we redirect back * This includes form validation, if it fails, we redirect back
* to the form with appropriate error messages. * to the form with appropriate error messages.
* Always return true if the current form action is exempt from validation * Always return true if the current form action is exempt from validation
* *
* Triggered through {@link httpSubmission()}. * Triggered through {@link httpSubmission()}.
* *
* *
* Note that CSRF protection takes place in {@link httpSubmission()}, * Note that CSRF protection takes place in {@link httpSubmission()},
* if it fails the form data will never reach this method. * if it fails the form data will never reach this method.
* *
* @return boolean * @return ValidationResult
*/ */
public function validate(){ public function validationResult()
$result = $this->validationResult(); {
// Opportunity to invalidate via validator
$action = $this->buttonClicked();
if ($action && $this->actionIsValidationExempt($action)) {
return ValidationResult::create();
}
// Valid // Invoke validator
if($result->valid()) { if ($this->validator) {
return true; $result = $this->validator->validate();
$this->loadMessagesFrom($result);
return $result;
}
// Invalid // Successful result
} else { return ValidationResult::create();
$this->saveFormErrorsToSession($result, $this->getData()); }
return false;
}
}
/** const MERGE_DEFAULT = 0;
* Experimental method - return a ValidationResult for the validator const MERGE_CLEAR_MISSING = 1;
* @return [type] [description] const MERGE_IGNORE_FALSEISH = 2;
*/
private function validationResult() {
// Start with a "valid" validation result
$result = ValidationResult::create();
// Opportunity to invalidate via validator /**
$action = $this->buttonClicked(); * Load data from the given DataObject or array.
if($action && $this->actionIsValidationExempt($action)) { *
return $result; * It will call $object->MyField to get the value of MyField.
} * If you passed an array, it will call $object[MyField].
* Doesn't save into dataless FormFields ({@link DatalessField}),
if($this->validator){ * as determined by {@link FieldList->dataFields()}.
$errors = $this->validator->validate(); *
* By default, if a field isn't set (as determined by isset()),
// Convert the old-style Validator result into a ValidationResult * its value will not be saved to the field, retaining
if($errors){ * potential existing values.
foreach($errors as $error) { *
$result->addFieldError($error['fieldName'], $error['message'], $error['messageType']); * Passed data should not be escaped, and is saved to the FormField instances unescaped.
} * Escaping happens automatically on saving the data through {@link saveInto()}.
} *
} * Escaping happens automatically on saving the data through
* {@link saveInto()}.
return $result; *
} * @uses FieldList->dataFields()
* @uses FormField->setValue()
const MERGE_DEFAULT = 0; *
const MERGE_CLEAR_MISSING = 1; * @param array|DataObject $data
const MERGE_IGNORE_FALSEISH = 2; * @param int $mergeStrategy
* For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
/** * what that property/key's value is.
* Load data from the given DataObject or array. *
* * By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
* It will call $object->MyField to get the value of MyField. * value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
* If you passed an array, it will call $object[MyField]. * "left alone", meaning they retain any previous value.
* Doesn't save into dataless FormFields ({@link DatalessField}), *
* as determined by {@link FieldList->dataFields()}. * You can pass a bitmask here to change this behaviour.
* *
* By default, if a field isn't set (as determined by isset()), * Passing CLEAR_MISSING means that any fields that don't match any property/key in
* its value will not be saved to the field, retaining * {@link $data} are cleared.
* potential existing values. *
* * Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
* Passed data should not be escaped, and is saved to the FormField instances unescaped. * a field's value.
* Escaping happens automatically on saving the data through {@link saveInto()}. *
* * For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
* Escaping happens automatically on saving the data through * CLEAR_MISSING
* {@link saveInto()}. *
* * @param array $fieldList An optional list of fields to process. This can be useful when you have a
* @uses FieldList->dataFields() * form that has some fields that save to one object, and some that save to another.
* @uses FormField->setValue() * @return $this
* */
* @param array|DataObject $data
* @param int $mergeStrategy
* For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
* what that property/key's value is.
*
* By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s
* value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are
* "left alone", meaning they retain any previous value.
*
* You can pass a bitmask here to change this behaviour.
*
* Passing CLEAR_MISSING means that any fields that don't match any property/key in
* {@link $data} are cleared.
*
* Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace
* a field's value.
*
* For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing
* CLEAR_MISSING
*
* @param array $fieldList An optional list of fields to process. This can be useful when you have a
* form that has some fields that save to one object, and some that save to another.
* @return Form
*/
public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
{ {
if(!is_object($data) && !is_array($data)) { if (!is_object($data) && !is_array($data)) {
user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING); user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING);
return $this; return $this;
} }
// Handle the backwards compatible case of passing "true" as the second argument // Handle the backwards compatible case of passing "true" as the second argument
if ($mergeStrategy === true) { if ($mergeStrategy === true) {
$mergeStrategy = self::MERGE_CLEAR_MISSING; $mergeStrategy = self::MERGE_CLEAR_MISSING;
} elseif ($mergeStrategy === false) { } elseif ($mergeStrategy === false) {
$mergeStrategy = 0; $mergeStrategy = 0;
} }
// if an object is passed, save it for historical reference through {@link getRecord()} // if an object is passed, save it for historical reference through {@link getRecord()}
if (is_object($data)) { if (is_object($data)) {
$this->record = $data; $this->record = $data;
} }
// dont include fields without data // dont include fields without data
$dataFields = $this->Fields()->dataFields(); $dataFields = $this->Fields()->dataFields();
if ($dataFields) { if (!$dataFields) {
foreach ($dataFields as $field) { return $this;
$name = $field->getName(); }
// Skip fields that have been excluded /** @var FormField $field */
if($fieldList && !in_array($name, $fieldList)) { foreach ($dataFields as $field) {
continue; $name = $field->getName();
}
// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value // Skip fields that have been excluded
if (is_array($data) && isset($data[$name . '_unchanged'])) { if ($fieldList && !in_array($name, $fieldList)) {
continue; continue;
}
// First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value
if (is_array($data) && isset($data[$name . '_unchanged'])) {
continue;
}
// Does this property exist on $data?
$exists = false;
// The value from $data for this field
$val = null;
if (is_object($data)) {
$exists = (
isset($data->$name) ||
$data->hasMethod($name) ||
($data->hasMethod('hasField') && $data->hasField($name))
);
if ($exists) {
$val = $data->__get($name);
} }
} elseif (is_array($data)) {
if (array_key_exists($name, $data)) {
$exists = true;
$val = $data[$name];
} // If field is in array-notation we need to access nested data
elseif (strpos($name, '[')) {
// First encode data using PHP's method of converting nested arrays to form data
$flatData = urldecode(http_build_query($data));
// Then pull the value out from that flattened string
preg_match('/' . addcslashes($name, '[]') . '=([^&]*)/', $flatData, $matches);
// Does this property exist on $data? if (isset($matches[1])) {
$exists = false; $exists = true;
// The value from $data for this field $val = $matches[1];
$val = null; }
}
}
if(is_object($data)) { // save to the field if either a value is given, or loading of blank/undefined values is forced
$exists = ( if ($exists) {
isset($data->$name) || if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH) {
$data->hasMethod($name) || // pass original data as well so composite fields can act on the additional information
($data->hasMethod('hasField') && $data->hasField($name))
);
if ($exists) {
$val = $data->__get($name);
}
} elseif (is_array($data)) {
if(array_key_exists($name, $data)) {
$exists = true;
$val = $data[$name];
} // If field is in array-notation we need to access nested data
else if(strpos($name,'[')) {
// First encode data using PHP's method of converting nested arrays to form data
$flatData = urldecode(http_build_query($data));
// Then pull the value out from that flattened string
preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches);
if (isset($matches[1])) {
$exists = true;
$val = $matches[1];
}
}
}
// save to the field if either a value is given, or loading of blank/undefined values is forced
if($exists){
if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){
// pass original data as well so composite fields can act on the additional information
$field->setValue($val, $data);
}
} elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
$field->setValue($val, $data); $field->setValue($val, $data);
} }
} } elseif (($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING) {
} $field->setValue($val, $data);
}
}
return $this;
}
return $this; /**
} * Save the contents of this form into the given data object.
* It will make use of setCastedField() to do this.
/** *
* Save the contents of this form into the given data object. * @param DataObjectInterface $dataObject The object to save data into
* It will make use of setCastedField() to do this. * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a
* * form that has some fields that save to one object, and some that save to another.
* @param DataObjectInterface $dataObject The object to save data into */
* @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a
* form that has some fields that save to one object, and some that save to another.
*/
public function saveInto(DataObjectInterface $dataObject, $fieldList = null) public function saveInto(DataObjectInterface $dataObject, $fieldList = null)
{ {
$dataFields = $this->fields->saveableFields(); $dataFields = $this->fields->saveableFields();
$lastField = null; $lastField = null;
if ($dataFields) { if ($dataFields) {
foreach ($dataFields as $field) { foreach ($dataFields as $field) {
// Skip fields that have been excluded // Skip fields that have been excluded
if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) { if ($fieldList && is_array($fieldList) && !in_array($field->getName(), $fieldList)) {
continue; continue;
} }
$saveMethod = "save{$field->getName()}";
$saveMethod = "save{$field->getName()}"; if ($field->getName() == "ClassName") {
$lastField = $field;
if($field->getName() == "ClassName"){ } elseif ($dataObject->hasMethod($saveMethod)) {
$lastField = $field; $dataObject->$saveMethod($field->dataValue());
}else if( $dataObject->hasMethod( $saveMethod ) ){ } elseif ($field->getName() !== "ID") {
$dataObject->$saveMethod( $field->dataValue()); $field->saveInto($dataObject);
} else if($field->getName() != "ID"){ }
$field->saveInto($dataObject); }
}
}
} }
if ($lastField) { if ($lastField) {
$lastField->saveInto($dataObject); $lastField->saveInto($dataObject);
} }
} }
/** /**
* Get the submitted data from this form through * Get the submitted data from this form through
* {@link FieldList->dataFields()}, which filters out * {@link FieldList->dataFields()}, which filters out
* any form-specific data like form-actions. * any form-specific data like form-actions.
* Calls {@link FormField->dataValue()} on each field, * Calls {@link FormField->dataValue()} on each field,
* which returns a value suitable for insertion into a DataObject * which returns a value suitable for insertion into a DataObject
* property. * property.
* *
* @return array * @return array
*/ */
public function getData() public function getData()
{ {
$dataFields = $this->fields->dataFields(); $dataFields = $this->fields->dataFields();
$data = array(); $data = array();
if($dataFields){ if ($dataFields) {
foreach($dataFields as $field) { foreach ($dataFields as $field) {
if($field->getName()) { if ($field->getName()) {
$data[$field->getName()] = $field->dataValue(); $data[$field->getName()] = $field->dataValue();
} }
} }
} }
return $data; return $data;
} }
/** /**
* Return a rendered version of this form. * Return a rendered version of this form.
* *
* This is returned when you access a form as $FormObject rather * This is returned when you access a form as $FormObject rather
* than <% with FormObject %> * than <% with FormObject %>
* *
* @return DBHTMLText * @return DBHTMLText
*/ */
public function forTemplate() public function forTemplate()
{ {
$return = $this->renderWith($this->getTemplates()); $return = $this->renderWith($this->getTemplates());
// Now that we're rendered, clear message // Now that we're rendered, clear message
$this->clearMessage(); $this->clearMessage();
return $return; return $return;
} }
/** /**
* Return a rendered version of this form, suitable for ajax post-back. * Return a rendered version of this form, suitable for ajax post-back.
* *
* It triggers slightly different behaviour, such as disabling the rewriting * It triggers slightly different behaviour, such as disabling the rewriting
* of # links. * of # links.
* *
* @return DBHTMLText * @return DBHTMLText
*/ */
public function forAjaxTemplate() public function forAjaxTemplate()
{ {
$view = new SSViewer($this->getTemplates()); $view = new SSViewer($this->getTemplates());
$return = $view->dontRewriteHashlinks()->process($this); $return = $view->dontRewriteHashlinks()->process($this);
// Now that we're rendered, clear message // Now that we're rendered, clear message
$this->clearMessage(); $this->clearMessage();
return $return; return $return;
} }
/** /**
* Returns an HTML rendition of this form, without the <form> tag itself. * Returns an HTML rendition of this form, without the <form> tag itself.
* *
* Attaches 3 extra hidden files, _form_action, _form_name, _form_method, * Attaches 3 extra hidden files, _form_action, _form_name, _form_method,
* and _form_enctype. These are the attributes of the form. These fields * and _form_enctype. These are the attributes of the form. These fields
* can be used to send the form to Ajax. * can be used to send the form to Ajax.
* *
* @deprecated 5.0 * @deprecated 5.0
* @return string * @return string
*/ */
public function formHtmlContent() public function formHtmlContent()
{ {
Deprecation::notice('5.0'); Deprecation::notice('5.0');
$this->IncludeFormTag = false; $this->IncludeFormTag = false;
$content = $this->forTemplate(); $content = $this->forTemplate();
$this->IncludeFormTag = true; $this->IncludeFormTag = true;
$content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\"" $content .= "<input type=\"hidden\" name=\"_form_action\" id=\"" . $this->FormName . "_form_action\""
. " value=\"" . $this->FormAction() . "\" />\n"; . " value=\"" . $this->FormAction() . "\" />\n";
$content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n"; $content .= "<input type=\"hidden\" name=\"_form_name\" value=\"" . $this->FormName() . "\" />\n";
$content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n"; $content .= "<input type=\"hidden\" name=\"_form_method\" value=\"" . $this->FormMethod() . "\" />\n";
$content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n"; $content .= "<input type=\"hidden\" name=\"_form_enctype\" value=\"" . $this->getEncType() . "\" />\n";
return $content; return $content;
} }
/** /**
* Render this form using the given template, and return the result as a string * Render this form using the given template, and return the result as a string
* You can pass either an SSViewer or a template name * You can pass either an SSViewer or a template name
* @param string|array $template * @param string|array $template
* @return DBHTMLText * @return DBHTMLText
*/ */
public function renderWithoutActionButton($template) public function renderWithoutActionButton($template)
{ {
$custom = $this->customise(array( $custom = $this->customise(array(
"Actions" => "", "Actions" => "",
)); ));
if(is_string($template)) { if (is_string($template)) {
$template = new SSViewer($template); $template = new SSViewer($template);
} }
return $template->process($custom); return $template->process($custom);
} }
/** /**
* Sets the button that was clicked. This should only be called by the Controller. * Sets the button that was clicked. This should only be called by the Controller.
* *
* @param callable $funcName The name of the action method that will be called. * @param callable $funcName The name of the action method that will be called.
* @return $this * @return $this
*/ */
public function setButtonClicked($funcName) public function setButtonClicked($funcName)
{ {
$this->buttonClickedFunc = $funcName; $this->buttonClickedFunc = $funcName;
return $this; return $this;
} }
/** /**
* @return FormAction * @return FormAction
*/ */
public function buttonClicked() public function buttonClicked()
{ {
$actions = $this->getAllActions(); $actions = $this->getAllActions();
foreach ($actions as $action) { foreach ($actions as $action) {
if ($this->buttonClickedFunc === $action->actionName()) { if ($this->buttonClickedFunc === $action->actionName()) {
return $action; return $action;
} }
} }
return null; return null;
} }
/** /**
* Get a list of all actions, including those in the main "fields" FieldList * Get a list of all actions, including those in the main "fields" FieldList
* *
* @return array * @return array
*/ */
protected function getAllActions() protected function getAllActions()
{ {
$fields = $this->fields->dataFields() ?: array(); $fields = $this->fields->dataFields() ?: array();
$actions = $this->actions->dataFields() ?: array(); $actions = $this->actions->dataFields() ?: array();
$fieldsAndActions = array_merge($fields, $actions); $fieldsAndActions = array_merge($fields, $actions);
$actions = array_filter($fieldsAndActions, function($fieldOrAction) { $actions = array_filter($fieldsAndActions, function ($fieldOrAction) {
return $fieldOrAction instanceof FormAction; return $fieldOrAction instanceof FormAction;
}); });
return $actions; return $actions;
} }
/** /**
* Return the default button that should be clicked when another one isn't * Return the default button that should be clicked when another one isn't
* available. * available.
* *
* @return FormAction * @return FormAction
*/ */
public function defaultAction() public function defaultAction()
{ {
if($this->hasDefaultAction && $this->actions) { if ($this->hasDefaultAction && $this->actions) {
return $this->actions->first(); return $this->actions->first();
} }
return null; return null;
} }
/** /**
* Disable the default button. * Disable the default button.
* *
* Ordinarily, when a form is processed and no action_XXX button is * Ordinarily, when a form is processed and no action_XXX button is
* available, then the first button in the actions list will be pressed. * available, then the first button in the actions list will be pressed.
* However, if this is "delete", for example, this isn't such a good idea. * However, if this is "delete", for example, this isn't such a good idea.
* *
* @return Form * @return Form
*/ */
public function disableDefaultAction() public function disableDefaultAction()
{ {
$this->hasDefaultAction = false; $this->hasDefaultAction = false;
return $this; return $this;
} }
/** /**
* Disable the requirement of a security token on this form instance. This * Disable the requirement of a security token on this form instance. This
* security protects against CSRF attacks, but you should disable this if * security protects against CSRF attacks, but you should disable this if
* you don't want to tie a form to a session - eg a search form. * you don't want to tie a form to a session - eg a search form.
* *
* Check for token state with {@link getSecurityToken()} and * Check for token state with {@link getSecurityToken()} and
* {@link SecurityToken->isEnabled()}. * {@link SecurityToken->isEnabled()}.
* *
* @return Form * @return Form
*/ */
public function disableSecurityToken() public function disableSecurityToken()
{ {
$this->securityToken = new NullSecurityToken(); $this->securityToken = new NullSecurityToken();
return $this; return $this;
} }
/** /**
* Enable {@link SecurityToken} protection for this form instance. * Enable {@link SecurityToken} protection for this form instance.
* *
* Check for token state with {@link getSecurityToken()} and * Check for token state with {@link getSecurityToken()} and
* {@link SecurityToken->isEnabled()}. * {@link SecurityToken->isEnabled()}.
* *
* @return Form * @return Form
*/ */
public function enableSecurityToken() public function enableSecurityToken()
{ {
$this->securityToken = new SecurityToken(); $this->securityToken = new SecurityToken();
return $this; return $this;
} }
/** /**
* Returns the security token for this form (if any exists). * Returns the security token for this form (if any exists).
* *
* Doesn't check for {@link securityTokenEnabled()}. * Doesn't check for {@link securityTokenEnabled()}.
* *
* Use {@link SecurityToken::inst()} to get a global token. * Use {@link SecurityToken::inst()} to get a global token.
* *
* @return SecurityToken|null * @return SecurityToken|null
*/ */
public function getSecurityToken() public function getSecurityToken()
{ {
return $this->securityToken; return $this->securityToken;
} }
/** /**
* Compiles all CSS-classes. * Compiles all CSS-classes.
* *
* @return string * @return string
*/ */
public function extraClass() public function extraClass()
{ {
return implode(array_unique($this->extraClasses), ' '); return implode(array_unique($this->extraClasses), ' ');
} }
/** /**
* Add a CSS-class to the form-container. If needed, multiple classes can * Add a CSS-class to the form-container. If needed, multiple classes can
* be added by delimiting a string with spaces. * be added by delimiting a string with spaces.
* *
* @param string $class A string containing a classname or several class * @param string $class A string containing a classname or several class
* names delimited by a single space. * names delimited by a single space.
* @return $this * @return $this
*/ */
public function addExtraClass($class) public function addExtraClass($class)
{ {
//split at white space //split at white space
$classes = preg_split('/\s+/', $class); $classes = preg_split('/\s+/', $class);
foreach($classes as $class) { foreach ($classes as $class) {
//add classes one by one //add classes one by one
$this->extraClasses[$class] = $class; $this->extraClasses[$class] = $class;
} }
return $this; return $this;
} }
/** /**
* Remove a CSS-class from the form-container. Multiple class names can * Remove a CSS-class from the form-container. Multiple class names can
* be passed through as a space delimited string * be passed through as a space delimited string
* *
* @param string $class * @param string $class
* @return $this * @return $this
*/ */
public function removeExtraClass($class) public function removeExtraClass($class)
{ {
//split at white space //split at white space
$classes = preg_split('/\s+/', $class); $classes = preg_split('/\s+/', $class);
foreach ($classes as $class) { foreach ($classes as $class) {
//unset one by one //unset one by one
unset($this->extraClasses[$class]); unset($this->extraClasses[$class]);
} }
return $this; return $this;
} }
public function debug() public function debug()
{ {
$result = "<h3>$this->class</h3><ul>"; $result = "<h3>$this->class</h3><ul>";
foreach($this->fields as $field) { foreach ($this->fields as $field) {
$result .= "<li>$field" . $field->debug() . "</li>"; $result .= "<li>$field" . $field->debug() . "</li>";
} }
$result .= "</ul>"; $result .= "</ul>";
if( $this->validator ) { if ($this->validator) {
/** @skipUpgrade */ /** @skipUpgrade */
$result .= '<h3>' . _t('Form.VALIDATOR', 'Validator') . '</h3>' . $this->validator->debug(); $result .= '<h3>'._t('Form.VALIDATOR', 'Validator').'</h3>' . $this->validator->debug();
} }
return $result; return $result;
} }
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// TESTING HELPERS // TESTING HELPERS
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** /**
* Test a submission of this form. * Test a submission of this form.
* @param string $action * @param string $action
* @param array $data * @param array $data
* @return HTTPResponse the response object that the handling controller produces. You can interrogate this in * @return HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test. * your unit test.
* @throws HTTPResponse_Exception * @throws HTTPResponse_Exception
*/ */
public function testSubmission($action, $data) public function testSubmission($action, $data)
{ {
$data['action_' . $action] = true; $data['action_' . $action] = true;
return Director::test($this->FormAction(), $data, Controller::curr()->getSession()); return Director::test($this->FormAction(), $data, Controller::curr()->getSession());
} }
/** /**
* Test an ajax submission of this form. * Test an ajax submission of this form.
* *
* @param string $action * @param string $action
* @param array $data * @param array $data
* @return HTTPResponse the response object that the handling controller produces. You can interrogate this in * @return HTTPResponse the response object that the handling controller produces. You can interrogate this in
* your unit test. * your unit test.
*/ */
public function testAjaxSubmission($action, $data) public function testAjaxSubmission($action, $data)
{ {
$data['ajax'] = 1; $data['ajax'] = 1;
return $this->testSubmission($action, $data); return $this->testSubmission($action, $data);
} }
} }

View File

@ -40,6 +40,7 @@ use SilverStripe\View\SSViewer;
*/ */
class FormField extends RequestHandler class FormField extends RequestHandler
{ {
use FormMessage;
/** @see $schemaDataType */ /** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRING = 'String'; const SCHEMA_DATA_TYPE_STRING = 'String';
@ -103,16 +104,6 @@ class FormField extends RequestHandler
*/ */
protected $value; protected $value;
/**
* @var string
*/
protected $message;
/**
* @var string
*/
protected $messageType;
/** /**
* @var string * @var string
*/ */
@ -274,8 +265,6 @@ class FormField extends RequestHandler
'HolderID' => 'Text', 'HolderID' => 'Text',
'Title' => 'Text', 'Title' => 'Text',
'RightTitle' => 'Text', 'RightTitle' => 'Text',
'MessageType' => 'Text',
'Message' => 'HTMLFragment',
'Description' => 'HTMLFragment', 'Description' => 'HTMLFragment',
); );
@ -456,32 +445,6 @@ class FormField extends RequestHandler
return $this->name; return $this->name;
} }
/**
* Returns the field message, used by form validation.
*
* Use {@link setError()} to set this property.
*
* @return string
*/
public function Message()
{
return $this->message;
}
/**
* Returns the field message type.
*
* Arbitrary value which is mostly used for CSS classes in the rendered HTML, e.g "required".
*
* Use {@link setError()} to set this property.
*
* @return string
*/
public function MessageType()
{
return $this->messageType;
}
/** /**
* Returns the field value. * Returns the field value.
* *
@ -613,8 +576,8 @@ class FormField extends RequestHandler
// e.g. red borders on input tags. // e.g. red borders on input tags.
// //
// CSS class needs to be different from the one rendered through {@link FieldHolder()}. // CSS class needs to be different from the one rendered through {@link FieldHolder()}.
if ($this->Message()) { if ($this->getMessage()) {
$classes[] .= 'holder-' . $this->MessageType(); $classes[] .= 'holder-' . $this->getMessageType();
} }
return implode(' ', $classes); return implode(' ', $classes);
@ -871,22 +834,13 @@ class FormField extends RequestHandler
return $form->getSecurityToken()->isEnabled(); return $form->getSecurityToken()->isEnabled();
} }
/** public function castingHelper($field)
* Sets the error message to be displayed on the form field.
*
* Allows HTML content, so remember to use Convert::raw2xml().
*
* @param string $message
* @param string $messageType
*
* @return $this
*/
public function setError($message, $messageType)
{ {
$this->message = $message; // Override casting for field message
$this->messageType = $messageType; if (strcasecmp($field, 'Message') === 0 && ($helper = $this->getMessageCastingHelper())) {
return $helper;
return $this; }
return parent::castingHelper($field);
} }
/** /**
@ -1211,12 +1165,12 @@ class FormField extends RequestHandler
*/ */
public function performReadonlyTransformation() public function performReadonlyTransformation()
{ {
$readonlyClassName = $this->class . '_Readonly'; $readonlyClassName = static::class . '_Readonly';
if (ClassInfo::exists($readonlyClassName)) { if (ClassInfo::exists($readonlyClassName)) {
$clone = $this->castedCopy($readonlyClassName); $clone = $this->castedCopy($readonlyClassName);
} else { } else {
$clone = $this->castedCopy('SilverStripe\\Forms\\ReadonlyField'); $clone = $this->castedCopy(ReadonlyField::class);
} }
$clone->setReadonly(true); $clone->setReadonly(true);
@ -1606,17 +1560,10 @@ class FormField extends RequestHandler
'name' => $this->getName(), 'name' => $this->getName(),
'id' => $this->ID(), 'id' => $this->ID(),
'value' => $this->Value(), 'value' => $this->Value(),
'message' => null, 'message' => $this->getSchemaMessage(),
'data' => [], 'data' => [],
]; ];
if ($message = $this->Message()) {
$state['message'] = [
'value' => ['html' => $message],
'type' => $this->MessageType(),
];
}
return $state; return $state;
} }

132
src/Forms/FormMessage.php Normal file
View File

@ -0,0 +1,132 @@
<?php
namespace SilverStripe\Forms;
use InvalidArgumentException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\ViewableData;
/**
* Form component which contains a castable message
*
* @mixin ViewableData
*/
trait FormMessage
{
/**
* @var string
*/
protected $message = '';
/**
* @var string
*/
protected $messageType = '';
/**
* Casting for message
*
* @var string
*/
protected $messageCast = null;
/**
* Returns the field message, used by form validation.
*
* Use {@link setError()} to set this property.
*
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* Returns the field message type.
*
* Arbitrary value which is mostly used for CSS classes in the rendered HTML, e.g "required".
*
* Use {@link setError()} to set this property.
*
* @return string
*/
public function getMessageType()
{
return $this->messageType;
}
/**
* Casting type for this message. Will be 'text' or 'html'
*
* @return string
*/
public function getMessageCast()
{
return $this->messageCast;
}
/**
* Sets the error message to be displayed on the form field.
*
* Allows HTML content, so remember to use Convert::raw2xml().
*
* @param string $message Message string
* @param string $messageType Message type
* @param string $messageCast
* @return $this
*/
public function setMessage(
$message,
$messageType = ValidationResult::TYPE_ERROR,
$messageCast = ValidationResult::CAST_TEXT
) {
if (!in_array($messageCast, [ValidationResult::CAST_TEXT, ValidationResult::CAST_HTML])) {
throw new InvalidArgumentException("Invalid message cast type");
}
$this->message = $message;
$this->messageType = $messageType;
$this->messageCast = $messageCast;
return $this;
}
/**
* Get casting helper for message cast, or null if not known
*
* @return string
*/
protected function getMessageCastingHelper()
{
switch ($this->getMessageCast()) {
case ValidationResult::CAST_TEXT:
return 'Text';
case ValidationResult::CAST_HTML:
return 'HTMLFragment';
default:
return null;
}
}
/**
* Get form schema encoded message
*
* @return array|null Message in array format, or null if no message
*/
public function getSchemaMessage()
{
$message = $this->getMessage();
if (!$message) {
return null;
}
// Form schema messages treat simple strings as plain text, so nest for html messages
if ($this->getMessageCast() === ValidationResult::CAST_HTML) {
$message = ['html' => $message];
}
return [
'value' => $message,
'type' => $this->getMessageType(),
];
}
}

View File

@ -24,101 +24,101 @@ use SilverStripe\ORM\ValidationException;
class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_ActionProvider class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_ActionProvider
{ {
/** /**
* If this is set to true, this {@link GridField_ActionProvider} will * If this is set to true, this {@link GridField_ActionProvider} will
* remove the object from the list, instead of deleting. * remove the object from the list, instead of deleting.
* *
* In the case of a has one, has many or many many list it will uncouple * In the case of a has one, has many or many many list it will uncouple
* the item from the list. * the item from the list.
* *
* @var boolean * @var boolean
*/ */
protected $removeRelation = false; protected $removeRelation = false;
/** /**
* *
* @param boolean $removeRelation - true if removing the item from the list, but not deleting it * @param boolean $removeRelation - true if removing the item from the list, but not deleting it
*/ */
public function __construct($removeRelation = false) public function __construct($removeRelation = false)
{ {
$this->removeRelation = $removeRelation; $this->removeRelation = $removeRelation;
} }
/** /**
* Add a column 'Delete' * Add a column 'Delete'
* *
* @param GridField $gridField * @param GridField $gridField
* @param array $columns * @param array $columns
*/ */
public function augmentColumns($gridField, &$columns) public function augmentColumns($gridField, &$columns)
{ {
if(!in_array('Actions', $columns)) { if (!in_array('Actions', $columns)) {
$columns[] = 'Actions'; $columns[] = 'Actions';
} }
} }
/** /**
* Return any special attributes that will be used for FormField::create_tag() * Return any special attributes that will be used for FormField::create_tag()
* *
* @param GridField $gridField * @param GridField $gridField
* @param DataObject $record * @param DataObject $record
* @param string $columnName * @param string $columnName
* @return array * @return array
*/ */
public function getColumnAttributes($gridField, $record, $columnName) public function getColumnAttributes($gridField, $record, $columnName)
{ {
return array('class' => 'grid-field__col-compact'); return array('class' => 'grid-field__col-compact');
} }
/** /**
* Add the title * Add the title
* *
* @param GridField $gridField * @param GridField $gridField
* @param string $columnName * @param string $columnName
* @return array * @return array
*/ */
public function getColumnMetadata($gridField, $columnName) public function getColumnMetadata($gridField, $columnName)
{ {
if($columnName == 'Actions') { if ($columnName == 'Actions') {
return array('title' => ''); return array('title' => '');
} }
} }
/** /**
* Which columns are handled by this component * Which columns are handled by this component
* *
* @param GridField $gridField * @param GridField $gridField
* @return array * @return array
*/ */
public function getColumnsHandled($gridField) public function getColumnsHandled($gridField)
{ {
return array('Actions'); return array('Actions');
} }
/** /**
* Which GridField actions are this component handling * Which GridField actions are this component handling
* *
* @param GridField $gridField * @param GridField $gridField
* @return array * @return array
*/ */
public function getActions($gridField) public function getActions($gridField)
{ {
return array('deleterecord', 'unlinkrelation'); return array('deleterecord', 'unlinkrelation');
} }
/** /**
* *
* @param GridField $gridField * @param GridField $gridField
* @param DataObject $record * @param DataObject $record
* @param string $columnName * @param string $columnName
* @return string the HTML for the column * @return string the HTML for the column
*/ */
public function getColumnContent($gridField, $record, $columnName) public function getColumnContent($gridField, $record, $columnName)
{ {
if($this->removeRelation) { if ($this->removeRelation) {
if(!$record->canEdit()) { if (!$record->canEdit()) {
return null; return null;
} }
$field = GridField_FormAction::create( $field = GridField_FormAction::create(
$gridField, $gridField,
@ -127,12 +127,12 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
"unlinkrelation", "unlinkrelation",
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
) )
->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink') ->addExtraClass('btn btn--no-text btn--icon-md font-icon-link-broken grid-field__icon-action gridfield-button-unlink')
->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink")); ->setAttribute('title', _t('GridAction.UnlinkRelation', "Unlink"));
} else { } else {
if(!$record->canDelete()) { if (!$record->canDelete()) {
return null; return null;
} }
$field = GridField_FormAction::create( $field = GridField_FormAction::create(
$gridField, $gridField,
@ -141,46 +141,48 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
"deleterecord", "deleterecord",
array('RecordID' => $record->ID) array('RecordID' => $record->ID)
) )
->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action') ->addExtraClass('gridfield-button-delete btn--icon-md font-icon-trash-bin btn--no-text grid-field__icon-action')
->setAttribute('title', _t('GridAction.Delete', "Delete")) ->setAttribute('title', _t('GridAction.Delete', "Delete"))
->setDescription(_t('GridAction.DELETE_DESCRIPTION','Delete')); ->setDescription(_t('GridAction.DELETE_DESCRIPTION', 'Delete'));
} }
return $field->Field(); return $field->Field();
} }
/** /**
* Handle the actions and apply any changes to the GridField * Handle the actions and apply any changes to the GridField
* *
* @param GridField $gridField * @param GridField $gridField
* @param string $actionName * @param string $actionName
* @param mixed $arguments * @param mixed $arguments
* @param array $data - form data * @param array $data - form data
* @throws ValidationException * @throws ValidationException
*/ */
public function handleAction(GridField $gridField, $actionName, $arguments, $data) public function handleAction(GridField $gridField, $actionName, $arguments, $data)
{ {
if($actionName == 'deleterecord' || $actionName == 'unlinkrelation') { if ($actionName == 'deleterecord' || $actionName == 'unlinkrelation') {
/** @var DataObject $item */ /** @var DataObject $item */
$item = $gridField->getList()->byID($arguments['RecordID']); $item = $gridField->getList()->byID($arguments['RecordID']);
if(!$item) { if (!$item) {
return; return;
} }
if($actionName == 'deleterecord') { if ($actionName == 'deleterecord') {
if(!$item->canDelete()) { if (!$item->canDelete()) {
throw new ValidationException( throw new ValidationException(
_t('GridFieldAction_Delete.DeletePermissionsFailure',"No delete permissions")); _t('GridFieldAction_Delete.DeletePermissionsFailure', "No delete permissions")
} );
}
$item->delete(); $item->delete();
} else { } else {
if(!$item->canEdit()) { if (!$item->canEdit()) {
throw new ValidationException( throw new ValidationException(
_t('GridFieldAction_Delete.EditPermissionsFailure',"No permission to unlink record")); _t('GridFieldAction_Delete.EditPermissionsFailure', "No permission to unlink record")
} );
}
$gridField->getList()->remove($item); $gridField->getList()->remove($item);
} }
} }
} }
} }

View File

@ -4,11 +4,9 @@ namespace SilverStripe\Forms\GridField;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Control\RequestHandler; use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -20,626 +18,592 @@ use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
class GridFieldDetailForm_ItemRequest extends RequestHandler class GridFieldDetailForm_ItemRequest extends RequestHandler
{ {
private static $allowed_actions = array( private static $allowed_actions = array(
'edit', 'edit',
'view', 'view',
'ItemEditForm' 'ItemEditForm'
); );
/** /**
* *
* @var GridField * @var GridField
*/ */
protected $gridField; protected $gridField;
/** /**
* *
* @var GridFieldDetailForm * @var GridFieldDetailForm
*/ */
protected $component; protected $component;
/** /**
* *
* @var DataObject * @var DataObject
*/ */
protected $record; protected $record;
/** /**
* This represents the current parent RequestHandler (which does not necessarily need to be a Controller). * This represents the current parent RequestHandler (which does not necessarily need to be a Controller).
* It allows us to traverse the RequestHandler chain upwards to reach the Controller stack. * It allows us to traverse the RequestHandler chain upwards to reach the Controller stack.
* *
* @var RequestHandler * @var RequestHandler
*/ */
protected $popupController; protected $popupController;
/** /**
* *
* @var string * @var string
*/ */
protected $popupFormName; protected $popupFormName;
/** /**
* @var String * @var String
*/ */
protected $template = null; protected $template = null;
private static $url_handlers = array( private static $url_handlers = array(
'$Action!' => '$Action', '$Action!' => '$Action',
'' => 'edit', '' => 'edit',
); );
/** /**
* *
* @param GridField $gridField * @param GridField $gridField
* @param GridFieldDetailForm $component * @param GridFieldDetailForm $component
* @param DataObject $record * @param DataObject $record
* @param RequestHandler $requestHandler * @param RequestHandler $requestHandler
* @param string $popupFormName * @param string $popupFormName
*/ */
public function __construct($gridField, $component, $record, $requestHandler, $popupFormName) public function __construct($gridField, $component, $record, $requestHandler, $popupFormName)
{ {
$this->gridField = $gridField; $this->gridField = $gridField;
$this->component = $component; $this->component = $component;
$this->record = $record; $this->record = $record;
$this->popupController = $requestHandler; $this->popupController = $requestHandler;
$this->popupFormName = $popupFormName; $this->popupFormName = $popupFormName;
parent::__construct(); parent::__construct();
} }
public function Link($action = null) public function Link($action = null)
{ {
return Controller::join_links( return Controller::join_links(
$this->gridField->Link('item'), $this->gridField->Link('item'),
$this->record->ID ? $this->record->ID : 'new', $this->record->ID ? $this->record->ID : 'new',
$action $action
); );
} }
/** /**
* @param HTTPRequest $request * @param HTTPRequest $request
* @return mixed * @return mixed
*/ */
public function view($request) public function view($request)
{ {
if (!$this->record->canView()) { if (!$this->record->canView()) {
$this->httpError(403); $this->httpError(403);
} }
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$form = $this->ItemEditForm(); $form = $this->ItemEditForm();
$form->makeReadonly(); $form->makeReadonly();
$data = new ArrayData(array( $data = new ArrayData(array(
'Backlink' => $controller->Link(), 'Backlink' => $controller->Link(),
'ItemEditForm' => $form 'ItemEditForm' => $form
)); ));
$return = $data->renderWith($this->getTemplates()); $return = $data->renderWith($this->getTemplates());
if ($request->isAjax()) { if ($request->isAjax()) {
return $return; return $return;
} else { } else {
return $controller->customise(array('Content' => $return)); return $controller->customise(array('Content' => $return));
} }
} }
/** /**
* @param HTTPRequest $request * @param HTTPRequest $request
* @return mixed * @return mixed
*/ */
public function edit($request) public function edit($request)
{ {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$form = $this->ItemEditForm(); $form = $this->ItemEditForm();
$return = $this->customise(array( $return = $this->customise(array(
'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(), 'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
'ItemEditForm' => $form, 'ItemEditForm' => $form,
))->renderWith($this->getTemplates()); ))->renderWith($this->getTemplates());
if ($request->isAjax()) { if ($request->isAjax()) {
return $return; return $return;
} else { } else {
// If not requested by ajax, we need to render it within the controller context+template // If not requested by ajax, we need to render it within the controller context+template
return $controller->customise(array( return $controller->customise(array(
// TODO CMS coupling // TODO CMS coupling
'Content' => $return, 'Content' => $return,
)); ));
} }
} }
/** /**
* Builds an item edit form. The arguments to getCMSFields() are the popupController and * Builds an item edit form. The arguments to getCMSFields() are the popupController and
* popupFormName, however this is an experimental API and may change. * popupFormName, however this is an experimental API and may change.
* *
* @todo In the future, we will probably need to come up with a tigher object representing a partially * @todo In the future, we will probably need to come up with a tigher object representing a partially
* complete controller with gaps for extra functionality. This, for example, would be a better way * complete controller with gaps for extra functionality. This, for example, would be a better way
* of letting Security/login put its log-in form inside a UI specified elsewhere. * of letting Security/login put its log-in form inside a UI specified elsewhere.
* *
* @return Form * @return Form
*/ */
public function ItemEditForm() public function ItemEditForm()
{ {
$list = $this->gridField->getList(); $list = $this->gridField->getList();
if (empty($this->record)) { if (empty($this->record)) {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
$url = $controller->getRequest()->getURL(); $url = $controller->getRequest()->getURL();
$noActionURL = $controller->removeAction($url); $noActionURL = $controller->removeAction($url);
$controller->getResponse()->removeHeader('Location'); //clear the existing redirect $controller->getResponse()->removeHeader('Location'); //clear the existing redirect
return $controller->redirect($noActionURL, 302); return $controller->redirect($noActionURL, 302);
} }
$canView = $this->record->canView(); $canView = $this->record->canView();
$canEdit = $this->record->canEdit(); $canEdit = $this->record->canEdit();
$canDelete = $this->record->canDelete(); $canDelete = $this->record->canDelete();
$canCreate = $this->record->canCreate(); $canCreate = $this->record->canCreate();
if (!$canView) { if (!$canView) {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
// TODO More friendly error // TODO More friendly error
return $controller->httpError(403); return $controller->httpError(403);
} }
// Build actions // Build actions
$actions = $this->getFormActions(); $actions = $this->getFormActions();
// If we are creating a new record in a has-many list, then // If we are creating a new record in a has-many list, then
// pre-populate the record's foreign key. // pre-populate the record's foreign key.
if ($list instanceof HasManyList && !$this->record->isInDB()) { if ($list instanceof HasManyList && !$this->record->isInDB()) {
$key = $list->getForeignKey(); $key = $list->getForeignKey();
$id = $list->getForeignID(); $id = $list->getForeignID();
$this->record->$key = $id; $this->record->$key = $id;
} }
$fields = $this->component->getFields(); $fields = $this->component->getFields();
if (!$fields) { if (!$fields) {
$fields = $this->record->getCMSFields(); $fields = $this->record->getCMSFields();
} }
// If we are creating a new record in a has-many list, then // If we are creating a new record in a has-many list, then
// Disable the form field as it has no effect. // Disable the form field as it has no effect.
if ($list instanceof HasManyList) { if ($list instanceof HasManyList) {
$key = $list->getForeignKey(); $key = $list->getForeignKey();
if ($field = $fields->dataFieldByName($key)) { if ($field = $fields->dataFieldByName($key)) {
$fields->makeFieldReadonly($field); $fields->makeFieldReadonly($field);
} }
} }
// Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead. // Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead.
// Thanks to this however, we are able to nest GridFields, and also access the initial Controller by // Thanks to this however, we are able to nest GridFields, and also access the initial Controller by
// dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController // dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController
// below. // below.
$form = new Form( $form = new Form(
$this, $this,
'ItemEditForm', 'ItemEditForm',
$fields, $fields,
$actions, $actions,
$this->component->getValidator() $this->component->getValidator()
); );
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT); $form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
if ($this->record->ID && !$canEdit) { if ($this->record->ID && !$canEdit) {
// Restrict editing of existing records // Restrict editing of existing records
$form->makeReadonly(); $form->makeReadonly();
// Hack to re-enable delete button if user can delete // Hack to re-enable delete button if user can delete
if ($canDelete) { if ($canDelete) {
$form->Actions()->fieldByName('action_doDelete')->setReadonly(false); $form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
} }
} elseif (!$this->record->ID && !$canCreate) { } elseif (!$this->record->ID && !$canCreate) {
// Restrict creation of new records // Restrict creation of new records
$form->makeReadonly(); $form->makeReadonly();
} }
// Load many_many extraData for record. // Load many_many extraData for record.
// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields(). // Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
if ($list instanceof ManyManyList) { if ($list instanceof ManyManyList) {
$extraData = $list->getExtraData('', $this->record->ID); $extraData = $list->getExtraData('', $this->record->ID);
$form->loadDataFrom(array('ManyMany' => $extraData)); $form->loadDataFrom(array('ManyMany' => $extraData));
} }
// TODO Coupling with CMS // TODO Coupling with CMS
$toplevelController = $this->getToplevelController(); $toplevelController = $this->getToplevelController();
if ($toplevelController && $toplevelController instanceof LeftAndMain) { if ($toplevelController && $toplevelController instanceof LeftAndMain) {
// Always show with base template (full width, no other panels), // Always show with base template (full width, no other panels),
// regardless of overloaded CMS controller templates. // regardless of overloaded CMS controller templates.
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller // TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
$form->setTemplate([ $form->setTemplate([
'type' => 'Includes', 'type' => 'Includes',
'SilverStripe\\Admin\\LeftAndMain_EditForm', 'SilverStripe\\Admin\\LeftAndMain_EditForm',
]); ]);
$form->addExtraClass('cms-content cms-edit-form center fill-height flexbox-area-grow'); $form->addExtraClass('cms-content cms-edit-form center fill-height flexbox-area-grow');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content'); $form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
if ($form->Fields()->hasTabSet()) { if ($form->Fields()->hasTabSet()) {
$form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet'); $form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
$form->addExtraClass('cms-tabset'); $form->addExtraClass('cms-tabset');
} }
$form->Backlink = $this->getBackLink(); $form->Backlink = $this->getBackLink();
} }
$cb = $this->component->getItemEditFormCallback(); $cb = $this->component->getItemEditFormCallback();
if ($cb) { if ($cb) {
$cb($form, $this); $cb($form, $this);
} }
$this->extend("updateItemEditForm", $form); $this->extend("updateItemEditForm", $form);
return $form; return $form;
} }
/** /**
* Build the set of form field actions for this DataObject * Build the set of form field actions for this DataObject
* *
* @return FieldList * @return FieldList
*/ */
protected function getFormActions() protected function getFormActions()
{ {
$canEdit = $this->record->canEdit(); $canEdit = $this->record->canEdit();
$canDelete = $this->record->canDelete(); $canDelete = $this->record->canDelete();
$actions = new FieldList(); $actions = new FieldList();
if ($this->record->ID !== 0) { if ($this->record->ID !== 0) {
if ($canEdit) { if ($canEdit) {
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save')) $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
->setUseButtonTag(true) ->setUseButtonTag(true)
->addExtraClass('ss-ui-action-constructive') ->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'accept')); ->setAttribute('data-icon', 'accept'));
} }
if ($canDelete) { if ($canDelete) {
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete')) $actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
->setUseButtonTag(true) ->setUseButtonTag(true)
->addExtraClass('ss-ui-action-destructive action-delete')); ->addExtraClass('ss-ui-action-destructive action-delete'));
} }
} else { // adding new record } else { // adding new record
//Change the Save label to 'Create' //Change the Save label to 'Create'
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create')) $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
->setUseButtonTag(true) ->setUseButtonTag(true)
->addExtraClass('ss-ui-action-constructive') ->addExtraClass('ss-ui-action-constructive')
->setAttribute('data-icon', 'add')); ->setAttribute('data-icon', 'add'));
// Add a Cancel link which is a button-like link and link back to one level up. // Add a Cancel link which is a button-like link and link back to one level up.
$crumbs = $this->Breadcrumbs(); $crumbs = $this->Breadcrumbs();
if ($crumbs && $crumbs->count() >= 2) { if ($crumbs && $crumbs->count() >= 2) {
$oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2); $oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2);
$text = sprintf( $text = sprintf(
"<a class=\"%s\" href=\"%s\">%s</a>", "<a class=\"%s\" href=\"%s\">%s</a>",
"crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes "crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
$oneLevelUp->Link, // url $oneLevelUp->Link, // url
_t('GridFieldDetailForm.CancelBtn', 'Cancel') // label _t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
); );
$actions->push(new LiteralField('cancelbutton', $text)); $actions->push(new LiteralField('cancelbutton', $text));
} }
} }
$this->extend('updateFormActions', $actions); $this->extend('updateFormActions', $actions);
return $actions; return $actions;
} }
/** /**
* Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest. * Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest.
* This allows us to access the Controller responsible for invoking the top-level GridField. * This allows us to access the Controller responsible for invoking the top-level GridField.
* This should be equivalent to getting the controller off the top of the controller stack via Controller::curr(), * This should be equivalent to getting the controller off the top of the controller stack via Controller::curr(),
* but allows us to avoid accessing the global state. * but allows us to avoid accessing the global state.
* *
* GridFieldDetailForm_ItemRequests are RequestHandlers, and as such they are not part of the controller stack. * GridFieldDetailForm_ItemRequests are RequestHandlers, and as such they are not part of the controller stack.
* *
* @return Controller * @return Controller
*/ */
protected function getToplevelController() protected function getToplevelController()
{ {
$c = $this->popupController; $c = $this->popupController;
while ($c && $c instanceof GridFieldDetailForm_ItemRequest) { while ($c && $c instanceof GridFieldDetailForm_ItemRequest) {
$c = $c->getController(); $c = $c->getController();
} }
return $c; return $c;
} }
protected function getBackLink() protected function getBackLink()
{ {
// TODO Coupling with CMS // TODO Coupling with CMS
$backlink = ''; $backlink = '';
$toplevelController = $this->getToplevelController(); $toplevelController = $this->getToplevelController();
if ($toplevelController && $toplevelController instanceof LeftAndMain) { if ($toplevelController && $toplevelController instanceof LeftAndMain) {
if ($toplevelController->hasMethod('Backlink')) { if ($toplevelController->hasMethod('Backlink')) {
$backlink = $toplevelController->Backlink(); $backlink = $toplevelController->Backlink();
} elseif ($this->popupController->hasMethod('Breadcrumbs')) { } elseif ($this->popupController->hasMethod('Breadcrumbs')) {
$parents = $this->popupController->Breadcrumbs(false)->items; $parents = $this->popupController->Breadcrumbs(false)->items;
$backlink = array_pop($parents)->Link; $backlink = array_pop($parents)->Link;
} }
} }
if (!$backlink) { if (!$backlink) {
$backlink = $toplevelController->Link(); $backlink = $toplevelController->Link();
} }
return $backlink; return $backlink;
} }
/** /**
* Get the list of extra data from the $record as saved into it by * Get the list of extra data from the $record as saved into it by
* {@see Form::saveInto()} * {@see Form::saveInto()}
* *
* Handles detection of falsey values explicitly saved into the * Handles detection of falsey values explicitly saved into the
* DataObject by formfields * DataObject by formfields
* *
* @param DataObject $record * @param DataObject $record
* @param SS_List $list * @param SS_List $list
* @return array List of data to write to the relation * @return array List of data to write to the relation
*/ */
protected function getExtraSavedData($record, $list) protected function getExtraSavedData($record, $list)
{ {
// Skip extra data if not ManyManyList // Skip extra data if not ManyManyList
if (!($list instanceof ManyManyList)) { if (!($list instanceof ManyManyList)) {
return null; return null;
} }
$data = array(); $data = array();
foreach ($list->getExtraFields() as $field => $dbSpec) { foreach ($list->getExtraFields() as $field => $dbSpec) {
$savedField = "ManyMany[{$field}]"; $savedField = "ManyMany[{$field}]";
if ($record->hasField($savedField)) { if ($record->hasField($savedField)) {
$data[$field] = $record->getField($savedField); $data[$field] = $record->getField($savedField);
} }
} }
return $data; return $data;
} }
public function doSave($data, $form) public function doSave($data, $form)
{ {
$isNewRecord = $this->record->ID == 0; $isNewRecord = $this->record->ID == 0;
// Check permission // Check permission
if (!$this->record->canEdit()) { if (!$this->record->canEdit()) {
return $this->httpError(403); return $this->httpError(403);
} }
// Save from form data // Save from form data
try { $this->saveFormIntoRecord($data, $form);
$this->saveFormIntoRecord($data, $form);
} catch (ValidationException $e) {
return $this->generateValidationResponse($form, $e);
}
$link = '<a href="' . $this->Link('edit') . '">"' $link = '<a href="' . $this->Link('edit') . '">"'
. htmlspecialchars($this->record->Title, ENT_QUOTES) . htmlspecialchars($this->record->Title, ENT_QUOTES)
. '"</a>'; . '"</a>';
$message = _t( $message = _t(
'GridFieldDetailForm.Saved', 'GridFieldDetailForm.Saved',
'Saved {name} {link}', 'Saved {name} {link}',
array( array(
'name' => $this->record->i18n_singular_name(), 'name' => $this->record->i18n_singular_name(),
'link' => $link 'link' => $link
) )
); );
$form->sessionMessage($message, 'good', false); $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
// Redirect after save // Redirect after save
return $this->redirectAfterSave($isNewRecord); return $this->redirectAfterSave($isNewRecord);
} }
/** /**
* Response object for this request after a successful save * Response object for this request after a successful save
* *
* @param bool $isNewRecord True if this record was just created * @param bool $isNewRecord True if this record was just created
* @return HTTPResponse|DBHTMLText * @return HTTPResponse|DBHTMLText
*/ */
protected function redirectAfterSave($isNewRecord) protected function redirectAfterSave($isNewRecord)
{ {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
if ($isNewRecord) { if ($isNewRecord) {
return $controller->redirect($this->Link()); return $controller->redirect($this->Link());
} elseif ($this->gridField->getList()->byID($this->record->ID)) { } elseif ($this->gridField->getList()->byID($this->record->ID)) {
// Return new view, as we can't do a "virtual redirect" via the CMS Ajax // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
// to the same URL (it assumes that its content is already current, and doesn't reload) // to the same URL (it assumes that its content is already current, and doesn't reload)
return $this->edit($controller->getRequest()); return $this->edit($controller->getRequest());
} else { } else {
// Changes to the record properties might've excluded the record from // Changes to the record properties might've excluded the record from
// a filtered list, so return back to the main view if it can't be found // a filtered list, so return back to the main view if it can't be found
$url = $controller->getRequest()->getURL(); $url = $controller->getRequest()->getURL();
$noActionURL = $controller->removeAction($url); $noActionURL = $controller->removeAction($url);
$controller->getRequest()->addHeader('X-Pjax', 'Content'); $controller->getRequest()->addHeader('X-Pjax', 'Content');
return $controller->redirect($noActionURL, 302); return $controller->redirect($noActionURL, 302);
} }
} }
public function httpError($errorCode, $errorMessage = null) public function httpError($errorCode, $errorMessage = null)
{ {
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
return $controller->httpError($errorCode, $errorMessage); return $controller->httpError($errorCode, $errorMessage);
} }
/** /**
* Loads the given form data into the underlying dataobject and relation * Loads the given form data into the underlying dataobject and relation
* *
* @param array $data * @param array $data
* @param Form $form * @param Form $form
* @throws ValidationException On error * @throws ValidationException On error
* @return DataObject Saved record * @return DataObject Saved record
*/ */
protected function saveFormIntoRecord($data, $form) protected function saveFormIntoRecord($data, $form)
{ {
$list = $this->gridField->getList(); $list = $this->gridField->getList();
// Check object matches the correct classname // Check object matches the correct classname
if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) { if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
$newClassName = $data['ClassName']; $newClassName = $data['ClassName'];
// The records originally saved attribute was overwritten by $form->saveInto($record) before. // The records originally saved attribute was overwritten by $form->saveInto($record) before.
// This is necessary for newClassInstance() to work as expected, and trigger change detection // This is necessary for newClassInstance() to work as expected, and trigger change detection
// on the ClassName attribute // on the ClassName attribute
$this->record->setClassName($this->record->ClassName); $this->record->setClassName($this->record->ClassName);
// Replace $record with a new instance // Replace $record with a new instance
$this->record = $this->record->newClassInstance($newClassName); $this->record = $this->record->newClassInstance($newClassName);
} }
// Save form and any extra saved data into this dataobject // Save form and any extra saved data into this dataobject
$form->saveInto($this->record); $form->saveInto($this->record);
$this->record->write(); $this->record->write();
$extraData = $this->getExtraSavedData($this->record, $list); $extraData = $this->getExtraSavedData($this->record, $list);
$list->add($this->record, $extraData); $list->add($this->record, $extraData);
return $this->record; return $this->record;
} }
/** /**
* Generate a response object for a form validation error * @param array $data
* * @param Form $form
* @param Form $form The source form * @return HTTPResponse
* @param ValidationException $e The validation error message * @throws ValidationException
* @return HTTPResponse */
* @throws HTTPResponse_Exception public function doDelete($data, $form)
*/ {
protected function generateValidationResponse($form, $e) $title = $this->record->Title;
{ if (!$this->record->canDelete()) {
$controller = $this->getToplevelController(); throw new ValidationException(
_t('GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
);
}
$this->record->delete();
$form->sessionMessage($e->getResult()->message(), 'bad', false); $message = sprintf(
$responseNegotiator = new PjaxResponseNegotiator(array( _t('GridFieldDetailForm.Deleted', 'Deleted %s %s'),
'CurrentForm' => function () use (&$form) { $this->record->i18n_singular_name(),
return $form->forTemplate(); htmlspecialchars($title, ENT_QUOTES)
}, );
'default' => function () use (&$controller) {
return $controller->redirectBack();
}
));
if ($controller->getRequest()->isAjax()) {
$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
}
return $responseNegotiator->respond($controller->getRequest());
}
/** $toplevelController = $this->getToplevelController();
* @param array $data if ($toplevelController && $toplevelController instanceof LeftAndMain) {
* @param Form $form $backForm = $toplevelController->getEditForm();
* @return HTTPResponse $backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
*/ } else {
public function doDelete($data, $form) $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
{ }
$title = $this->record->Title;
try {
if (!$this->record->canDelete()) {
throw new ValidationException(
_t('GridFieldDetailForm.DeletePermissionsFailure',"No delete permissions"));
}
$this->record->delete(); //when an item is deleted, redirect to the parent controller
} catch (ValidationException $e) { $controller = $this->getToplevelController();
$form->sessionMessage($e->getResult()->message(), 'bad', false); $controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
return $this->getToplevelController()->redirectBack();
}
$message = sprintf( return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
_t('GridFieldDetailForm.Deleted', 'Deleted %s %s'), }
$this->record->i18n_singular_name(),
htmlspecialchars($title, ENT_QUOTES)
);
$toplevelController = $this->getToplevelController(); /**
if ($toplevelController && $toplevelController instanceof LeftAndMain) { * @param string $template
$backForm = $toplevelController->getEditForm(); * @return $this
$backForm->sessionMessage($message, 'good', false); */
} else { public function setTemplate($template)
$form->sessionMessage($message, 'good', false); {
} $this->template = $template;
return $this;
}
//when an item is deleted, redirect to the parent controller /**
$controller = $this->getToplevelController(); * @return string
$controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh */
public function getTemplate()
{
return $this->template;
}
return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section /**
} * Get list of templates to use
*
* @return array
*/
public function getTemplates()
{
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
// Prefer any custom template
if ($this->getTemplate()) {
array_unshift($templates, $this->getTemplate());
}
return $templates;
}
/** /**
* @param string $template * @return Controller
* @return $this */
*/ public function getController()
public function setTemplate($template) {
{ return $this->popupController;
$this->template = $template; }
return $this;
}
/** /**
* @return string * @return GridField
*/ */
public function getTemplate() public function getGridField()
{ {
return $this->template; return $this->gridField;
} }
/** /**
* Get list of templates to use * @return DataObject
* */
* @return array public function getRecord()
*/ {
public function getTemplates() return $this->record;
{ }
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__);
// Prefer any custom template
if($this->getTemplate()) {
array_unshift($templates, $this->getTemplate());
}
return $templates;
}
/** /**
* @return Controller * CMS-specific functionality: Passes through navigation breadcrumbs
*/ * to the template, and includes the currently edited record (if any).
public function getController() * see {@link LeftAndMain->Breadcrumbs()} for details.
{ *
return $this->popupController; * @param boolean $unlinked
} * @return ArrayList
*/
public function Breadcrumbs($unlinked = false)
{
if (!$this->popupController->hasMethod('Breadcrumbs')) {
return null;
}
/** /** @var ArrayList $items */
* @return GridField $items = $this->popupController->Breadcrumbs($unlinked);
*/ if ($this->record && $this->record->ID) {
public function getGridField() $title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
{ $items->push(new ArrayData(array(
return $this->gridField; 'Title' => $title,
} 'Link' => $this->Link()
)));
} else {
$items->push(new ArrayData(array(
'Title' => sprintf(_t('GridField.NewRecord', 'New %s'), $this->record->i18n_singular_name()),
'Link' => false
)));
}
/** return $items;
* @return DataObject }
*/
public function getRecord()
{
return $this->record;
}
/**
* CMS-specific functionality: Passes through navigation breadcrumbs
* to the template, and includes the currently edited record (if any).
* see {@link LeftAndMain->Breadcrumbs()} for details.
*
* @param boolean $unlinked
* @return ArrayList
*/
public function Breadcrumbs($unlinked = false)
{
if (!$this->popupController->hasMethod('Breadcrumbs')) {
return null;
}
/** @var ArrayList $items */
$items = $this->popupController->Breadcrumbs($unlinked);
if ($this->record && $this->record->ID) {
$title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
$items->push(new ArrayData(array(
'Title' => $title,
'Link' => $this->Link()
)));
} else {
$items->push(new ArrayData(array(
'Title' => sprintf(_t('GridField.NewRecord', 'New %s'), $this->record->i18n_singular_name()),
'Link' => false
)));
}
return $items;
}
} }

View File

@ -15,8 +15,12 @@ use SilverStripe\ORM\ArrayLib;
class RequiredFields extends Validator class RequiredFields extends Validator
{ {
/**
* List of required fields
*
* @var array
*/
protected $required; protected $required;
protected $useLabels = true;
/** /**
* Pass each field to be validated as a seperate argument to the constructor * Pass each field to be validated as a seperate argument to the constructor
@ -84,56 +88,58 @@ class RequiredFields extends Validator
$valid = ($field->validate($this) && $valid); $valid = ($field->validate($this) && $valid);
} }
if ($this->required) { if (!$this->required) {
foreach ($this->required as $fieldName) { return $valid;
if (!$fieldName) { }
continue;
}
if ($fieldName instanceof FormField) { foreach ($this->required as $fieldName) {
$formField = $fieldName; if (!$fieldName) {
$fieldName = $fieldName->getName(); continue;
}
if ($fieldName instanceof FormField) {
$formField = $fieldName;
$fieldName = $fieldName->getName();
} else {
$formField = $fields->dataFieldByName($fieldName);
}
// 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;
} else { } else {
$formField = $fields->dataFieldByName($fieldName); $error = (count($value)) ? false : true;
} }
} else {
// assume a string or integer
$error = (strlen($value)) ? false : true;
}
// submitted data for file upload fields come back as an array if ($formField && $error) {
$value = isset($data[$fieldName]) ? $data[$fieldName] : null; $errorMessage = _t(
'Form.FIELDISREQUIRED',
if (is_array($value)) { '{name} is required',
if ($formField instanceof FileField && isset($value['error']) && $value['error']) { array(
$error = true; 'name' => strip_tags(
} else { '"' . ($formField->Title() ? $formField->Title() : $fieldName) . '"'
$error = (count($value)) ? false : true;
}
} else {
// assume a string or integer
$error = (strlen($value)) ? false : true;
}
if ($formField && $error) {
$errorMessage = _t(
'Form.FIELDISREQUIRED',
'{name} is required',
array(
'name' => strip_tags(
'"' . ($formField->Title() ? $formField->Title() : $fieldName) . '"'
)
) )
); )
);
if ($msg = $formField->getCustomValidationMessage()) { if ($msg = $formField->getCustomValidationMessage()) {
$errorMessage = $msg; $errorMessage = $msg;
}
$this->validationError(
$fieldName,
$errorMessage,
"required"
);
$valid = false;
} }
$this->validationError(
$fieldName,
$errorMessage,
"required"
);
$valid = false;
} }
} }

View File

@ -2,10 +2,12 @@
namespace SilverStripe\Forms\Schema; namespace SilverStripe\Forms\Schema;
use SilverStripe\Control\Session; use InvalidArgumentException;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\ORM\ValidationResult;
/** /**
* Represents a {@link Form} as structured data which allows a frontend library to render it. * Represents a {@link Form} as structured data which allows a frontend library to render it.
@ -14,6 +16,69 @@ use SilverStripe\Forms\FormField;
*/ */
class FormSchema class FormSchema
{ {
/**
* Request the schema part
*/
const PART_SCHEMA = 'schema';
/**
* Request the state part
*/
const PART_STATE = 'state';
/**
* Request the errors from a {@see ValidationResult}
*/
const PART_ERRORS = 'errors';
/**
* Request errors if invalid, or state if valid
*/
const PART_AUTO = 'auto';
/**
* Returns a representation of the provided {@link Form} as structured data,
* based on the request data.
*
* @param array|string $schemaParts Array or list of requested parts.
* @param string $schemaID ID for this schema. Required.
* @param Form $form Required for 'state' or 'schema' response
* @param ValidationResult $result Required for 'error' response
* @return array
*/
public function getMultipartSchema($schemaParts, $schemaID, Form $form = null, ValidationResult $result = null)
{
if (!is_array($schemaParts)) {
$schemaParts = preg_split('#\s*,\s*#', $schemaParts) ?: [];
}
$wantSchema = in_array('schema', $schemaParts);
$wantState = in_array('state', $schemaParts);
$wantErrors = in_array('errors', $schemaParts);
$auto = in_array('auto', $schemaParts);
// Require ID
if (empty($schemaID)) {
throw new InvalidArgumentException("schemaID is required");
}
$return = ['id' => $schemaID];
// Default to schema if not set
if ($form && ($wantSchema || empty($schemaParts))) {
$return['schema'] = $this->getSchema($form);
}
// Return 'state' if requested, or if there are errors and 'auto'
if ($form && ($wantState || ($auto && !$result))) {
$return['state'] = $this->getState($form);
}
// Return errors if 'errors' or 'auto'
if ($result && ($wantErrors || $auto)) {
$return['errors'] = $this->getErrors($result);
}
return $return;
}
/** /**
* Gets the schema for this form as a nested array. * Gets the schema for this form as a nested array.
@ -55,18 +120,9 @@ class FormSchema
*/ */
public function getState(Form $form) public function getState(Form $form)
{ {
// Ensure that session errors are populated within form field messages
$form->setupFormErrors();
// @todo - Replace with ValidationResult handling
// Currently tri-state; null (unsubmitted), true (submitted-valid), false (submitted-invalid)
$errors = Session::get("FormInfo.{$form->FormName()}.errors");
$valid = isset($errors) ? empty($errors) : null;
$state = [ $state = [
'id' => $form->FormName(), 'id' => $form->FormName(),
'fields' => [], 'fields' => [],
'valid' => $valid,
'messages' => [], 'messages' => [],
]; ];
@ -76,17 +132,46 @@ class FormSchema
$this->getFieldStates($form->Actions()) $this->getFieldStates($form->Actions())
); );
if ($message = $form->Message()) { if ($message = $form->getSchemaMessage()) {
$state['messages'][] = [ $state['messages'][] = $message;
// TODO Make form / field messages not always stored as html
'value' => ['html' => $message],
'type' => $form->MessageType(),
];
} }
return $state; return $state;
} }
/**
* @param ValidationResult $result
* @return array List of errors
*/
public function getErrors(ValidationResult $result)
{
$messages = [];
foreach ($result->getMessages() as $message) {
$messages[] = $this->getSchemaForMessage($message);
}
return $messages;
}
/**
* Return form schema for encoded validation message
*
* @param array $message Internal ValidationResult format for this message
* @return array Form schema format for this message
*/
protected function getSchemaForMessage($message)
{
// Form schema messages treat simple strings as plain text, so nest for html messages
$value = $message['message'];
if ($message['messageCast'] === ValidationResult::CAST_HTML) {
$value = ['html' => $message];
}
return [
'value' => $value,
'type' => $message['messageType'],
'field' => empty($message['fieldName']) ? null : $message['fieldName'],
];
}
protected function getFieldStates($fields) protected function getFieldStates($fields)
{ {
$states = []; $states = [];

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Forms; namespace SilverStripe\Forms;
use SilverStripe\Core\Object; use SilverStripe\Core\Object;
use SilverStripe\ORM\ValidationResult;
/** /**
* This validation class handles all form and custom form validation through the use of Required * This validation class handles all form and custom form validation through the use of Required
@ -12,40 +13,42 @@ use SilverStripe\Core\Object;
abstract class Validator extends Object abstract class Validator extends Object
{ {
public function __construct()
{
parent::__construct();
$this->resetResult();
}
/** /**
* @var Form $form * @var Form $form
*/ */
protected $form; protected $form;
/** /**
* @var array $errors * @var ValidationResult $result
*/ */
protected $errors; protected $result;
/** /**
* @param Form $form * @param Form $form
*
* @return $this * @return $this
*/ */
public function setForm($form) public function setForm($form)
{ {
$this->form = $form; $this->form = $form;
return $this; return $this;
} }
/** /**
* Returns any errors there may be. * Returns any errors there may be.
* *
* @return null|array * @return ValidationResult
*/ */
public function validate() public function validate()
{ {
$this->errors = null; $this->resetResult();
$this->php($this->form->getData()); $this->php($this->form->getData());
return $this->result;
return $this->errors;
} }
/** /**
@ -55,17 +58,22 @@ abstract class Validator extends Object
* *
* See {@link getErrors()} for details. * See {@link getErrors()} for details.
* *
* @param string $fieldName * @param string $fieldName Field name for this error
* @param string $errorMessage * @param string $message The message string
* @param string $errorMessageType * @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
* @return $this
*/ */
public function validationError($fieldName, $errorMessage, $errorMessageType = '') public function validationError(
{ $fieldName,
$this->errors[] = array( $message,
'fieldName' => $fieldName, $messageType = ValidationResult::TYPE_ERROR,
'message' => $errorMessage, $cast = ValidationResult::CAST_TEXT
'messageType' => $errorMessageType, ) {
); $this->result->addFieldError($fieldName, $message, $messageType, null, $cast);
return $this;
} }
/** /**
@ -77,6 +85,7 @@ abstract class Validator extends Object
* 'fieldName' => '[form field name]', * 'fieldName' => '[form field name]',
* 'message' => '[validation error message]', * 'message' => '[validation error message]',
* 'messageType' => '[bad|message|validation|required]', * 'messageType' => '[bad|message|validation|required]',
* 'messageCast' => '[text|html]'
* ) * )
* </code> * </code>
* *
@ -84,32 +93,20 @@ abstract class Validator extends Object
*/ */
public function getErrors() public function getErrors()
{ {
return $this->errors; if ($this->result) {
return $this->result->getMessages();
}
return null;
} }
/** /**
* @param string $fieldName * Get last validation result
* @param array $data *
* @return ValidationResult
*/ */
public function requireField($fieldName, $data) public function getResult()
{ {
if (is_array($data[$fieldName]) && count($data[$fieldName])) { return $this->result;
foreach ($data[$fieldName] as $componentKey => $componentValue) {
if (!strlen($componentValue)) {
$this->validationError(
$fieldName,
sprintf('%s %s is required', $fieldName, $componentKey),
'required'
);
}
}
} elseif (!strlen($data[$fieldName])) {
$this->validationError(
$fieldName,
sprintf('%s is required', $fieldName),
'required'
);
}
} }
/** /**
@ -131,4 +128,15 @@ abstract class Validator extends Object
* @return mixed * @return mixed
*/ */
abstract public function php($data); abstract public function php($data);
/**
* Clear current result
*
* @return $this
*/
protected function resetResult()
{
$this->result = ValidationResult::create();
return $this;
}
} }

View File

@ -459,7 +459,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if ($sourceObject->manyMany()) { if ($sourceObject->manyMany()) {
foreach ($sourceObject->manyMany() as $name => $type) { foreach ($sourceObject->manyMany() as $name => $type) {
//many_many include belongs_many_many //many_many include belongs_many_many
$this->duplicateRelations($sourceObject, $destinationObject, $name); $this->duplicateRelations($sourceObject, $destinationObject, $name);
} }
} }
@ -1139,11 +1139,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if ($defaults) { if ($defaults) {
foreach ($defaults as $fieldName => $fieldValue) { foreach ($defaults as $fieldName => $fieldValue) {
// SRM 2007-03-06: Stricter check // SRM 2007-03-06: Stricter check
if (!isset($this->$fieldName) || $this->$fieldName === null) { if (!isset($this->$fieldName) || $this->$fieldName === null) {
$this->$fieldName = $fieldValue; $this->$fieldName = $fieldValue;
} }
// Set many-many defaults with an array of ids // Set many-many defaults with an array of ids
if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) { if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
/** @var ManyManyList $manyManyJoin */ /** @var ManyManyList $manyManyJoin */
$manyManyJoin = $this->$fieldName(); $manyManyJoin = $this->$fieldName();
@ -1170,19 +1170,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if ($this->ObsoleteClassName) { if ($this->ObsoleteClassName) {
return new ValidationException( return new ValidationException(
"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ". "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
"you need to change the ClassName before you can write it", "you need to change the ClassName before you can write it"
E_USER_WARNING
); );
} }
if ($this->config()->get('validation_enabled')) { if ($this->config()->get('validation_enabled')) {
$result = $this->validate(); $result = $this->validate();
if (!$result->valid()) { if (!$result->isValid()) {
return new ValidationException( return new ValidationException($result);
$result,
$result->message(),
E_USER_WARNING
);
} }
} }
return null; return null;
@ -2356,7 +2351,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null, 'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null, 'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
'level' => $level 'level' => $level
); );
} }
} }
@ -3382,7 +3377,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
$types = array( $types = array(
'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED) 'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
); );
if ($includerelations) { if ($includerelations) {
$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED); $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED); $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);

View File

@ -25,199 +25,199 @@ use Exception;
class Hierarchy extends DataExtension class Hierarchy extends DataExtension
{ {
protected $markedNodes; protected $markedNodes;
protected $markingFilter; protected $markingFilter;
/** @var int */ /** @var int */
protected $_cache_numChildren; protected $_cache_numChildren;
/** /**
* The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least * The lower bounds for the amount of nodes to mark. If set, the logic will expand nodes until it reaches at least
* this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be * this number, and then stops. Root nodes will always show regardless of this settting. Further nodes can be
* lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30 * lazy-loaded via ajax. This isn't a hard limit. Example: On a value of 10, with 20 root nodes, each having 30
* children, the actual node count will be 50 (all root nodes plus first expanded child). * children, the actual node count will be 50 (all root nodes plus first expanded child).
* *
* @config * @config
* @var int * @var int
*/ */
private static $node_threshold_total = 50; private static $node_threshold_total = 50;
/** /**
* Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available * Limit on the maximum children a specific node can display. Serves as a hard limit to avoid exceeding available
* server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding * server resources in generating the tree, and browser resources in rendering it. Nodes with children exceeding
* this value typically won't display any children, although this is configurable through the $nodeCountCallback * this value typically won't display any children, although this is configurable through the $nodeCountCallback
* parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting. * parameter in {@link getChildrenAsUL()}. "Root" nodes will always show all children, regardless of this setting.
* *
* @config * @config
* @var int * @var int
*/ */
private static $node_threshold_leaf = 250; private static $node_threshold_leaf = 250;
/** /**
* A list of classnames to exclude from display in both the CMS and front end * A list of classnames to exclude from display in both the CMS and front end
* displays. ->Children() and ->AllChildren affected. * displays. ->Children() and ->AllChildren affected.
* Especially useful for big sets of pages like listings * Especially useful for big sets of pages like listings
* If you use this, and still need the classes to be editable * If you use this, and still need the classes to be editable
* then add a model admin for the class * then add a model admin for the class
* Note: Does not filter subclasses (non-inheriting) * Note: Does not filter subclasses (non-inheriting)
* *
* @var array * @var array
* @config * @config
*/ */
private static $hide_from_hierarchy = array(); private static $hide_from_hierarchy = array();
/** /**
* A list of classnames to exclude from display in the page tree views of the CMS, * A list of classnames to exclude from display in the page tree views of the CMS,
* unlike $hide_from_hierarchy above which effects both CMS and front end. * unlike $hide_from_hierarchy above which effects both CMS and front end.
* Especially useful for big sets of pages like listings * Especially useful for big sets of pages like listings
* If you use this, and still need the classes to be editable * If you use this, and still need the classes to be editable
* then add a model admin for the class * then add a model admin for the class
* Note: Does not filter subclasses (non-inheriting) * Note: Does not filter subclasses (non-inheriting)
* *
* @var array * @var array
* @config * @config
*/ */
private static $hide_from_cms_tree = array(); private static $hide_from_cms_tree = array();
public static function get_extra_config($class, $extension, $args) public static function get_extra_config($class, $extension, $args)
{ {
return array( return array(
'has_one' => array('Parent' => $class) 'has_one' => array('Parent' => $class)
); );
} }
/** /**
* Validate the owner object - check for existence of infinite loops. * Validate the owner object - check for existence of infinite loops.
* *
* @param ValidationResult $validationResult * @param ValidationResult $validationResult
*/ */
public function validate(ValidationResult $validationResult) public function validate(ValidationResult $validationResult)
{ {
// The object is new, won't be looping. // The object is new, won't be looping.
if (!$this->owner->ID) { if (!$this->owner->ID) {
return; return;
} }
// The object has no parent, won't be looping. // The object has no parent, won't be looping.
if (!$this->owner->ParentID) { if (!$this->owner->ParentID) {
return; return;
} }
// The parent has not changed, skip the check for performance reasons. // The parent has not changed, skip the check for performance reasons.
if (!$this->owner->isChanged('ParentID')) { if (!$this->owner->isChanged('ParentID')) {
return; return;
} }
// Walk the hierarchy upwards until we reach the top, or until we reach the originating node again. // Walk the hierarchy upwards until we reach the top, or until we reach the originating node again.
$node = $this->owner; $node = $this->owner;
while($node) { while ($node) {
if ($node->ParentID==$this->owner->ID) { if ($node->ParentID==$this->owner->ID) {
// Hierarchy is looping. // Hierarchy is looping.
$validationResult->addError( $validationResult->addError(
_t( _t(
'Hierarchy.InfiniteLoopNotAllowed', 'Hierarchy.InfiniteLoopNotAllowed',
'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this', 'Infinite loop found within the "{type}" hierarchy. Please change the parent to resolve this',
'First argument is the class that makes up the hierarchy.', 'First argument is the class that makes up the hierarchy.',
array('type' => $this->owner->class) array('type' => $this->owner->class)
), ),
'bad', 'bad',
'INFINITE_LOOP' 'INFINITE_LOOP'
); );
break; break;
} }
$node = $node->ParentID ? $node->Parent() : null; $node = $node->ParentID ? $node->Parent() : null;
} }
// At this point the $validationResult contains the response. // At this point the $validationResult contains the response.
} }
/** /**
* Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they * Returns the children of this DataObject as an XHTML UL. This will be called recursively on each child, so if they
* have children they will be displayed as a UL inside a LI. * have children they will be displayed as a UL inside a LI.
* *
* @param string $attributes Attributes to add to the UL * @param string $attributes Attributes to add to the UL
* @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>' * @param string|callable $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function * @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function
* @param bool $limitToMarked Display only marked children * @param bool $limitToMarked Display only marked children
* @param string $childrenMethod The name of the method used to get children from each object * @param string $childrenMethod The name of the method used to get children from each object
* @param string $numChildrenMethod * @param string $numChildrenMethod
* @param bool $rootCall Set to true for this first call, and then to false for calls inside the recursion. * @param bool $rootCall Set to true for this first call, and then to false for calls inside the recursion.
* You should not change this. * You should not change this.
* @param int $nodeCountThreshold See {@link self::$node_threshold_total} * @param int $nodeCountThreshold See {@link self::$node_threshold_total}
* @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity to * @param callable $nodeCountCallback Called with the node count, which gives the callback an opportunity to
* intercept the query. Useful e.g. to avoid excessive children listings (Arguments: $parent, $numChildren) * intercept the query. Useful e.g. to avoid excessive children listings (Arguments: $parent, $numChildren)
* @return string * @return string
*/ */
public function getChildrenAsUL( public function getChildrenAsUL(
$attributes = "", $attributes = "",
$titleEval = '"<li>" . $child->Title', $titleEval = '"<li>" . $child->Title',
$extraArg = null, $extraArg = null,
$limitToMarked = false, $limitToMarked = false,
$childrenMethod = "AllChildrenIncludingDeleted", $childrenMethod = "AllChildrenIncludingDeleted",
$numChildrenMethod = "numChildren", $numChildrenMethod = "numChildren",
$rootCall = true, $rootCall = true,
$nodeCountThreshold = null, $nodeCountThreshold = null,
$nodeCountCallback = null $nodeCountCallback = null
) { ) {
if(!is_numeric($nodeCountThreshold)) { if (!is_numeric($nodeCountThreshold)) {
$nodeCountThreshold = Config::inst()->get(__CLASS__, 'node_threshold_total'); $nodeCountThreshold = Config::inst()->get(__CLASS__, 'node_threshold_total');
} }
if($limitToMarked && $rootCall) { if ($limitToMarked && $rootCall) {
$this->markingFinished($numChildrenMethod); $this->markingFinished($numChildrenMethod);
} }
if($nodeCountCallback) { if ($nodeCountCallback) {
$nodeCountWarning = $nodeCountCallback($this->owner, $this->owner->$numChildrenMethod()); $nodeCountWarning = $nodeCountCallback($this->owner, $this->owner->$numChildrenMethod());
if ($nodeCountWarning) { if ($nodeCountWarning) {
return $nodeCountWarning; return $nodeCountWarning;
} }
} }
if($this->owner->hasMethod($childrenMethod)) { if ($this->owner->hasMethod($childrenMethod)) {
$children = $this->owner->$childrenMethod($extraArg); $children = $this->owner->$childrenMethod($extraArg);
} else { } else {
$children = null; $children = null;
user_error(sprintf( user_error(sprintf(
"Can't find the method '%s' on class '%s' for getting tree children", "Can't find the method '%s' on class '%s' for getting tree children",
$childrenMethod, $childrenMethod,
get_class($this->owner) get_class($this->owner)
), E_USER_ERROR); ), E_USER_ERROR);
} }
$output = null; $output = null;
if($children) { if ($children) {
if($attributes) { if ($attributes) {
$attributes = " $attributes"; $attributes = " $attributes";
} }
$output = "<ul$attributes>\n"; $output = "<ul$attributes>\n";
foreach($children as $child) { foreach ($children as $child) {
if(!$limitToMarked || $child->isMarked()) { if (!$limitToMarked || $child->isMarked()) {
$foundAChild = true; $foundAChild = true;
if(is_callable($titleEval)) { if (is_callable($titleEval)) {
$output .= $titleEval($child, $numChildrenMethod); $output .= $titleEval($child, $numChildrenMethod);
} else { } else {
$output .= eval("return $titleEval;"); $output .= eval("return $titleEval;");
} }
$output .= "\n"; $output .= "\n";
$numChildren = $child->$numChildrenMethod(); $numChildren = $child->$numChildrenMethod();
if (// Always traverse into opened nodes (they might be exposed as parents of search results) if (// Always traverse into opened nodes (they might be exposed as parents of search results)
$child->isExpanded() $child->isExpanded()
// Only traverse into children if we haven't reached the maximum node count already. // Only traverse into children if we haven't reached the maximum node count already.
// Otherwise, the remaining nodes are lazy loaded via ajax. // Otherwise, the remaining nodes are lazy loaded via ajax.
&& $child->isMarked() && $child->isMarked()
) { ) {
// Additionally check if node count requirements are met // Additionally check if node count requirements are met
$nodeCountWarning = $nodeCountCallback ? $nodeCountCallback($child, $numChildren) : null; $nodeCountWarning = $nodeCountCallback ? $nodeCountCallback($child, $numChildren) : null;
if($nodeCountWarning) { if ($nodeCountWarning) {
$output .= $nodeCountWarning; $output .= $nodeCountWarning;
$child->markClosed(); $child->markClosed();
} else { } else {
$output .= $child->getChildrenAsUL( $output .= $child->getChildrenAsUL(
"", "",
$titleEval, $titleEval,
@ -228,770 +228,770 @@ class Hierarchy extends DataExtension
false, false,
$nodeCountThreshold $nodeCountThreshold
); );
} }
} elseif($child->isTreeOpened()) { } elseif ($child->isTreeOpened()) {
// Since we're not loading children, don't mark it as open either // Since we're not loading children, don't mark it as open either
$child->markClosed(); $child->markClosed();
} }
$output .= "</li>\n"; $output .= "</li>\n";
} }
} }
$output .= "</ul>\n"; $output .= "</ul>\n";
} }
if(isset($foundAChild) && $foundAChild) { if (isset($foundAChild) && $foundAChild) {
return $output; return $output;
} }
return null; return null;
} }
/** /**
* Mark a segment of the tree, by calling mark(). * Mark a segment of the tree, by calling mark().
* *
* The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to * The method performs a breadth-first traversal until the number of nodes is more than minCount. This is used to
* get a limited number of tree nodes to show in the CMS initially. * get a limited number of tree nodes to show in the CMS initially.
* *
* This method returns the number of nodes marked. After this method is called other methods can check * This method returns the number of nodes marked. After this method is called other methods can check
* {@link isExpanded()} and {@link isMarked()} on individual nodes. * {@link isExpanded()} and {@link isMarked()} on individual nodes.
* *
* @param int $nodeCountThreshold See {@link getChildrenAsUL()} * @param int $nodeCountThreshold See {@link getChildrenAsUL()}
* @param mixed $context * @param mixed $context
* @param string $childrenMethod * @param string $childrenMethod
* @param string $numChildrenMethod * @param string $numChildrenMethod
* @return int The actual number of nodes marked. * @return int The actual number of nodes marked.
*/ */
public function markPartialTree( public function markPartialTree(
$nodeCountThreshold = 30, $nodeCountThreshold = 30,
$context = null, $context = null,
$childrenMethod = "AllChildrenIncludingDeleted", $childrenMethod = "AllChildrenIncludingDeleted",
$numChildrenMethod = "numChildren" $numChildrenMethod = "numChildren"
) { ) {
if (!is_numeric($nodeCountThreshold)) { if (!is_numeric($nodeCountThreshold)) {
$nodeCountThreshold = 30; $nodeCountThreshold = 30;
} }
$this->markedNodes = array($this->owner->ID => $this->owner); $this->markedNodes = array($this->owner->ID => $this->owner);
$this->owner->markUnexpanded(); $this->owner->markUnexpanded();
// foreach can't handle an ever-growing $nodes list // foreach can't handle an ever-growing $nodes list
while(list($id, $node) = each($this->markedNodes)) { while (list($id, $node) = each($this->markedNodes)) {
$children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod); $children = $this->markChildren($node, $context, $childrenMethod, $numChildrenMethod);
if($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) { if ($nodeCountThreshold && sizeof($this->markedNodes) > $nodeCountThreshold) {
// Undo marking children as opened since they're lazy loaded // Undo marking children as opened since they're lazy loaded
if ($children) { if ($children) {
foreach ($children as $child) { foreach ($children as $child) {
$child->markClosed(); $child->markClosed();
} }
} }
break; break;
} }
} }
return sizeof($this->markedNodes); return sizeof($this->markedNodes);
} }
/** /**
* Filter the marking to only those object with $node->$parameterName == $parameterValue * Filter the marking to only those object with $node->$parameterName == $parameterValue
* *
* @param string $parameterName The parameter on each node to check when marking. * @param string $parameterName The parameter on each node to check when marking.
* @param mixed $parameterValue The value the parameter must be to be marked. * @param mixed $parameterValue The value the parameter must be to be marked.
*/ */
public function setMarkingFilter($parameterName, $parameterValue) public function setMarkingFilter($parameterName, $parameterValue)
{ {
$this->markingFilter = array( $this->markingFilter = array(
"parameter" => $parameterName, "parameter" => $parameterName,
"value" => $parameterValue "value" => $parameterValue
); );
} }
/** /**
* Filter the marking to only those where the function returns true. The node in question will be passed to the * Filter the marking to only those where the function returns true. The node in question will be passed to the
* function. * function.
* *
* @param string $funcName The name of the function to call * @param string $funcName The name of the function to call
*/ */
public function setMarkingFilterFunction($funcName) public function setMarkingFilterFunction($funcName)
{ {
$this->markingFilter = array( $this->markingFilter = array(
"func" => $funcName, "func" => $funcName,
); );
} }
/** /**
* Returns true if the marking filter matches on the given node. * Returns true if the marking filter matches on the given node.
* *
* @param DataObject $node Node to check * @param DataObject $node Node to check
* @return bool * @return bool
*/ */
public function markingFilterMatches($node) public function markingFilterMatches($node)
{ {
if(!$this->markingFilter) { if (!$this->markingFilter) {
return true; return true;
} }
if(isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) { if (isset($this->markingFilter['parameter']) && $parameterName = $this->markingFilter['parameter']) {
if(is_array($this->markingFilter['value'])){ if (is_array($this->markingFilter['value'])) {
$ret = false; $ret = false;
foreach($this->markingFilter['value'] as $value) { foreach ($this->markingFilter['value'] as $value) {
$ret = $ret||$node->$parameterName==$value; $ret = $ret||$node->$parameterName==$value;
if($ret == true) { if ($ret == true) {
break; break;
} }
} }
return $ret; return $ret;
} else { } else {
return ($node->$parameterName == $this->markingFilter['value']); return ($node->$parameterName == $this->markingFilter['value']);
} }
} else if ($func = $this->markingFilter['func']) { } elseif ($func = $this->markingFilter['func']) {
return call_user_func($func, $node); return call_user_func($func, $node);
} }
} }
/** /**
* Mark all children of the given node that match the marking filter. * Mark all children of the given node that match the marking filter.
* *
* @param DataObject $node Parent node * @param DataObject $node Parent node
* @param mixed $context * @param mixed $context
* @param string $childrenMethod The name of the instance method to call to get the object's list of children * @param string $childrenMethod The name of the instance method to call to get the object's list of children
* @param string $numChildrenMethod The name of the instance method to call to count the object's children * @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return DataList * @return DataList
*/ */
public function markChildren( public function markChildren(
$node, $node,
$context = null, $context = null,
$childrenMethod = "AllChildrenIncludingDeleted", $childrenMethod = "AllChildrenIncludingDeleted",
$numChildrenMethod = "numChildren" $numChildrenMethod = "numChildren"
) { ) {
if($node->hasMethod($childrenMethod)) { if ($node->hasMethod($childrenMethod)) {
$children = $node->$childrenMethod($context); $children = $node->$childrenMethod($context);
} else { } else {
$children = null; $children = null;
user_error(sprintf( user_error(sprintf(
"Can't find the method '%s' on class '%s' for getting tree children", "Can't find the method '%s' on class '%s' for getting tree children",
$childrenMethod, $childrenMethod,
get_class($node) get_class($node)
), E_USER_ERROR); ), E_USER_ERROR);
} }
$node->markExpanded(); $node->markExpanded();
if($children) { if ($children) {
foreach($children as $child) { foreach ($children as $child) {
$markingMatches = $this->markingFilterMatches($child); $markingMatches = $this->markingFilterMatches($child);
if($markingMatches) { if ($markingMatches) {
// Mark a child node as unexpanded if it has children and has not already been expanded // Mark a child node as unexpanded if it has children and has not already been expanded
if($child->$numChildrenMethod() && !$child->isExpanded()) { if ($child->$numChildrenMethod() && !$child->isExpanded()) {
$child->markUnexpanded(); $child->markUnexpanded();
} else { } else {
$child->markExpanded(); $child->markExpanded();
} }
$this->markedNodes[$child->ID] = $child; $this->markedNodes[$child->ID] = $child;
} }
} }
} }
return $children; return $children;
} }
/** /**
* Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating * Ensure marked nodes that have children are also marked expanded. Call this after marking but before iterating
* over the tree. * over the tree.
* *
* @param string $numChildrenMethod The name of the instance method to call to count the object's children * @param string $numChildrenMethod The name of the instance method to call to count the object's children
*/ */
protected function markingFinished($numChildrenMethod = "numChildren") protected function markingFinished($numChildrenMethod = "numChildren")
{ {
// Mark childless nodes as expanded. // Mark childless nodes as expanded.
if($this->markedNodes) { if ($this->markedNodes) {
foreach($this->markedNodes as $id => $node) { foreach ($this->markedNodes as $id => $node) {
if(!$node->isExpanded() && !$node->$numChildrenMethod()) { if (!$node->isExpanded() && !$node->$numChildrenMethod()) {
$node->markExpanded(); $node->markExpanded();
} }
} }
} }
} }
/** /**
* Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the * Return CSS classes of 'unexpanded', 'closed', both, or neither, as well as a 'jstree-*' state depending on the
* marking of this DataObject. * marking of this DataObject.
* *
* @param string $numChildrenMethod The name of the instance method to call to count the object's children * @param string $numChildrenMethod The name of the instance method to call to count the object's children
* @return string * @return string
*/ */
public function markingClasses($numChildrenMethod = "numChildren") public function markingClasses($numChildrenMethod = "numChildren")
{ {
$classes = ''; $classes = '';
if(!$this->isExpanded()) { if (!$this->isExpanded()) {
$classes .= " unexpanded"; $classes .= " unexpanded";
} }
// Set jstree open state, or mark it as a leaf (closed) if there are no children // Set jstree open state, or mark it as a leaf (closed) if there are no children
if(!$this->owner->$numChildrenMethod()) { if (!$this->owner->$numChildrenMethod()) {
$classes .= " jstree-leaf closed"; $classes .= " jstree-leaf closed";
} elseif($this->isTreeOpened()) { } elseif ($this->isTreeOpened()) {
$classes .= " jstree-open"; $classes .= " jstree-open";
} else { } else {
$classes .= " jstree-closed closed"; $classes .= " jstree-closed closed";
} }
return $classes; return $classes;
} }
/** /**
* Mark the children of the DataObject with the given ID. * Mark the children of the DataObject with the given ID.
* *
* @param int $id ID of parent node * @param int $id ID of parent node
* @param bool $open If this is true, mark the parent node as opened * @param bool $open If this is true, mark the parent node as opened
* @return bool * @return bool
*/ */
public function markById($id, $open = false) public function markById($id, $open = false)
{ {
if(isset($this->markedNodes[$id])) { if (isset($this->markedNodes[$id])) {
$this->markChildren($this->markedNodes[$id]); $this->markChildren($this->markedNodes[$id]);
if($open) { if ($open) {
$this->markedNodes[$id]->markOpened(); $this->markedNodes[$id]->markOpened();
} }
return true; return true;
} else { } else {
return false; return false;
} }
} }
/** /**
* Expose the given object in the tree, by marking this page and all it ancestors. * Expose the given object in the tree, by marking this page and all it ancestors.
* *
* @param DataObject $childObj * @param DataObject $childObj
*/ */
public function markToExpose($childObj) public function markToExpose($childObj)
{ {
if(is_object($childObj)){ if (is_object($childObj)) {
$stack = array_reverse($childObj->parentStack()); $stack = array_reverse($childObj->parentStack());
foreach($stack as $stackItem) { foreach ($stack as $stackItem) {
$this->markById($stackItem->ID, true); $this->markById($stackItem->ID, true);
} }
} }
} }
/** /**
* Return the IDs of all the marked nodes. * Return the IDs of all the marked nodes.
* *
* @return array * @return array
*/ */
public function markedNodeIDs() public function markedNodeIDs()
{ {
return array_keys($this->markedNodes); return array_keys($this->markedNodes);
} }
/** /**
* Return an array of this page and its ancestors, ordered item -> root. * Return an array of this page and its ancestors, ordered item -> root.
* *
* @return SiteTree[] * @return SiteTree[]
*/ */
public function parentStack() public function parentStack()
{ {
$p = $this->owner; $p = $this->owner;
while($p) { while ($p) {
$stack[] = $p; $stack[] = $p;
$p = $p->ParentID ? $p->Parent() : null; $p = $p->ParentID ? $p->Parent() : null;
} }
return $stack; return $stack;
} }
/** /**
* Cache of DataObjects' marked statuses: [ClassName][ID] = bool * Cache of DataObjects' marked statuses: [ClassName][ID] = bool
* @var array * @var array
*/ */
protected static $marked = array(); protected static $marked = array();
/** /**
* Cache of DataObjects' expanded statuses: [ClassName][ID] = bool * Cache of DataObjects' expanded statuses: [ClassName][ID] = bool
* @var array * @var array
*/ */
protected static $expanded = array(); protected static $expanded = array();
/** /**
* Cache of DataObjects' opened statuses: [ClassName][ID] = bool * Cache of DataObjects' opened statuses: [ClassName][ID] = bool
* @var array * @var array
*/ */
protected static $treeOpened = array(); protected static $treeOpened = array();
/** /**
* Mark this DataObject as expanded. * Mark this DataObject as expanded.
*/ */
public function markExpanded() public function markExpanded()
{ {
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$expanded[$this->owner->baseClass()][$this->owner->ID] = true; self::$expanded[$this->owner->baseClass()][$this->owner->ID] = true;
} }
/** /**
* Mark this DataObject as unexpanded. * Mark this DataObject as unexpanded.
*/ */
public function markUnexpanded() public function markUnexpanded()
{ {
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$expanded[$this->owner->baseClass()][$this->owner->ID] = false; self::$expanded[$this->owner->baseClass()][$this->owner->ID] = false;
} }
/** /**
* Mark this DataObject's tree as opened. * Mark this DataObject's tree as opened.
*/ */
public function markOpened() public function markOpened()
{ {
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$treeOpened[$this->owner->baseClass()][$this->owner->ID] = true; self::$treeOpened[$this->owner->baseClass()][$this->owner->ID] = true;
} }
/** /**
* Mark this DataObject's tree as closed. * Mark this DataObject's tree as closed.
*/ */
public function markClosed() public function markClosed()
{ {
if(isset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID])) { if (isset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID])) {
unset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID]); unset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID]);
} }
} }
/** /**
* Check if this DataObject is marked. * Check if this DataObject is marked.
* *
* @return bool * @return bool
*/ */
public function isMarked() public function isMarked()
{ {
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false; return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
} }
/** /**
* Check if this DataObject is expanded. * Check if this DataObject is expanded.
* *
* @return bool * @return bool
*/ */
public function isExpanded() public function isExpanded()
{ {
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false; return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
} }
/** /**
* Check if this DataObject's tree is opened. * Check if this DataObject's tree is opened.
* *
* @return bool * @return bool
*/ */
public function isTreeOpened() public function isTreeOpened()
{ {
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false; return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
} }
/** /**
* Get a list of this DataObject's and all it's descendants IDs. * Get a list of this DataObject's and all it's descendants IDs.
* *
* @return int[] * @return int[]
*/ */
public function getDescendantIDList() public function getDescendantIDList()
{ {
$idList = array(); $idList = array();
$this->loadDescendantIDListInto($idList); $this->loadDescendantIDListInto($idList);
return $idList; return $idList;
} }
/** /**
* Get a list of this DataObject's and all it's descendants ID, and put them in $idList. * Get a list of this DataObject's and all it's descendants ID, and put them in $idList.
* *
* @param array $idList Array to put results in. * @param array $idList Array to put results in.
*/ */
public function loadDescendantIDListInto(&$idList) public function loadDescendantIDListInto(&$idList)
{ {
if($children = $this->AllChildren()) { if ($children = $this->AllChildren()) {
foreach($children as $child) { foreach ($children as $child) {
if(in_array($child->ID, $idList)) { if (in_array($child->ID, $idList)) {
continue; continue;
} }
$idList[] = $child->ID; $idList[] = $child->ID;
/** @var Hierarchy $ext */ /** @var Hierarchy $ext */
$ext = $child->getExtensionInstance('SilverStripe\ORM\Hierarchy\Hierarchy'); $ext = $child->getExtensionInstance('SilverStripe\ORM\Hierarchy\Hierarchy');
$ext->setOwner($child); $ext->setOwner($child);
$ext->loadDescendantIDListInto($idList); $ext->loadDescendantIDListInto($idList);
$ext->clearOwner(); $ext->clearOwner();
} }
} }
} }
/** /**
* Get the children for this DataObject. * Get the children for this DataObject.
* *
* @return DataList * @return DataList
*/ */
public function Children() public function Children()
{ {
if(!(isset($this->_cache_children) && $this->_cache_children)) { if (!(isset($this->_cache_children) && $this->_cache_children)) {
$result = $this->owner->stageChildren(false); $result = $this->owner->stageChildren(false);
$children = array(); $children = array();
foreach ($result as $record) { foreach ($result as $record) {
if ($record->canView()) { if ($record->canView()) {
$children[] = $record; $children[] = $record;
} }
} }
$this->_cache_children = new ArrayList($children); $this->_cache_children = new ArrayList($children);
} }
return $this->_cache_children; return $this->_cache_children;
} }
/** /**
* Return all children, including those 'not in menus'. * Return all children, including those 'not in menus'.
* *
* @return DataList * @return DataList
*/ */
public function AllChildren() public function AllChildren()
{ {
return $this->owner->stageChildren(true); return $this->owner->stageChildren(true);
} }
/** /**
* Return all children, including those that have been deleted but are still in live. * Return all children, including those that have been deleted but are still in live.
* - Deleted children will be marked as "DeletedFromStage" * - Deleted children will be marked as "DeletedFromStage"
* - Added children will be marked as "AddedToStage" * - Added children will be marked as "AddedToStage"
* - Modified children will be marked as "ModifiedOnStage" * - Modified children will be marked as "ModifiedOnStage"
* - Everything else has "SameOnStage" set, as an indicator that this information has been looked up. * - Everything else has "SameOnStage" set, as an indicator that this information has been looked up.
* *
* @param mixed $context * @param mixed $context
* @return ArrayList * @return ArrayList
*/ */
public function AllChildrenIncludingDeleted($context = null) public function AllChildrenIncludingDeleted($context = null)
{ {
return $this->doAllChildrenIncludingDeleted($context); return $this->doAllChildrenIncludingDeleted($context);
} }
/** /**
* @see AllChildrenIncludingDeleted * @see AllChildrenIncludingDeleted
* *
* @param mixed $context * @param mixed $context
* @return ArrayList * @return ArrayList
*/ */
public function doAllChildrenIncludingDeleted($context = null) public function doAllChildrenIncludingDeleted($context = null)
{ {
if (!$this->owner) { if (!$this->owner) {
user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner'); user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
} }
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
if($baseClass) { if ($baseClass) {
$stageChildren = $this->owner->stageChildren(true); $stageChildren = $this->owner->stageChildren(true);
// Add live site content that doesn't exist on the stage site, if required. // Add live site content that doesn't exist on the stage site, if required.
if($this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) { if ($this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
// Next, go through the live children. Only some of these will be listed // Next, go through the live children. Only some of these will be listed
$liveChildren = $this->owner->liveChildren(true, true); $liveChildren = $this->owner->liveChildren(true, true);
if($liveChildren) { if ($liveChildren) {
$merged = new ArrayList(); $merged = new ArrayList();
$merged->merge($stageChildren); $merged->merge($stageChildren);
$merged->merge($liveChildren); $merged->merge($liveChildren);
$stageChildren = $merged; $stageChildren = $merged;
} }
} }
$this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context); $this->owner->extend("augmentAllChildrenIncludingDeleted", $stageChildren, $context);
} else { } else {
user_error( user_error(
"Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'", "Hierarchy::AllChildren() Couldn't determine base class for '{$this->owner->class}'",
E_USER_ERROR E_USER_ERROR
); );
} }
return $stageChildren; return $stageChildren;
} }
/** /**
* Return all the children that this page had, including pages that were deleted from both stage & live. * Return all the children that this page had, including pages that were deleted from both stage & live.
* *
* @return DataList * @return DataList
* @throws Exception * @throws Exception
*/ */
public function AllHistoricalChildren() public function AllHistoricalChildren()
{ {
if(!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) { if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
} }
$baseTable = $this->owner->baseTable(); $baseTable = $this->owner->baseTable();
$parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID'); $parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID');
return Versioned::get_including_deleted( return Versioned::get_including_deleted(
$this->owner->baseClass(), $this->owner->baseClass(),
[ $parentIDColumn => $this->owner->ID ], [ $parentIDColumn => $this->owner->ID ],
"\"{$baseTable}\".\"ID\" ASC" "\"{$baseTable}\".\"ID\" ASC"
); );
} }
/** /**
* Return the number of children that this page ever had, including pages that were deleted. * Return the number of children that this page ever had, including pages that were deleted.
* *
* @return int * @return int
* @throws Exception * @throws Exception
*/ */
public function numHistoricalChildren() public function numHistoricalChildren()
{ {
if(!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) { if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
} }
return $this->AllHistoricalChildren()->count(); return $this->AllHistoricalChildren()->count();
} }
/** /**
* Return the number of direct children. By default, values are cached after the first invocation. Can be * Return the number of direct children. By default, values are cached after the first invocation. Can be
* augumented by {@link augmentNumChildrenCountQuery()}. * augumented by {@link augmentNumChildrenCountQuery()}.
* *
* @param bool $cache Whether to retrieve values from cache * @param bool $cache Whether to retrieve values from cache
* @return int * @return int
*/ */
public function numChildren($cache = true) public function numChildren($cache = true)
{ {
// Build the cache for this class if it doesn't exist. // Build the cache for this class if it doesn't exist.
if(!$cache || !is_numeric($this->_cache_numChildren)) { if (!$cache || !is_numeric($this->_cache_numChildren)) {
// Hey, this is efficient now! // Hey, this is efficient now!
// We call stageChildren(), because Children() has canView() filtering // We call stageChildren(), because Children() has canView() filtering
$this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count(); $this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count();
} }
// If theres no value in the cache, it just means that it doesn't have any children. // If theres no value in the cache, it just means that it doesn't have any children.
return $this->_cache_numChildren; return $this->_cache_numChildren;
} }
/** /**
* Checks if we're on a controller where we should filter. ie. Are we loading the SiteTree? * Checks if we're on a controller where we should filter. ie. Are we loading the SiteTree?
* *
* @return bool * @return bool
*/ */
public function showingCMSTree() public function showingCMSTree()
{ {
if (!Controller::has_curr()) { if (!Controller::has_curr()) {
return false; return false;
} }
$controller = Controller::curr(); $controller = Controller::curr();
return $controller instanceof LeftAndMain return $controller instanceof LeftAndMain
&& in_array($controller->getAction(), array("treeview", "listview", "getsubtree")); && in_array($controller->getAction(), array("treeview", "listview", "getsubtree"));
} }
/** /**
* Return children in the stage site. * Return children in the stage site.
* *
* @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when * @param bool $showAll Include all of the elements, even those not shown in the menus. Only applicable when
* extension is applied to {@link SiteTree}. * extension is applied to {@link SiteTree}.
* @return DataList * @return DataList
*/ */
public function stageChildren($showAll = false) public function stageChildren($showAll = false)
{ {
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy; $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree; $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
$staged = $baseClass::get() $staged = $baseClass::get()
->filter('ParentID', (int)$this->owner->ID) ->filter('ParentID', (int)$this->owner->ID)
->exclude('ID', (int)$this->owner->ID); ->exclude('ID', (int)$this->owner->ID);
if ($hide_from_hierarchy) { if ($hide_from_hierarchy) {
$staged = $staged->exclude('ClassName', $hide_from_hierarchy); $staged = $staged->exclude('ClassName', $hide_from_hierarchy);
} }
if ($hide_from_cms_tree && $this->showingCMSTree()) { if ($hide_from_cms_tree && $this->showingCMSTree()) {
$staged = $staged->exclude('ClassName', $hide_from_cms_tree); $staged = $staged->exclude('ClassName', $hide_from_cms_tree);
} }
if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
$staged = $staged->filter('ShowInMenus', 1); $staged = $staged->filter('ShowInMenus', 1);
} }
$this->owner->extend("augmentStageChildren", $staged, $showAll); $this->owner->extend("augmentStageChildren", $staged, $showAll);
return $staged; return $staged;
} }
/** /**
* Return children in the live site, if it exists. * Return children in the live site, if it exists.
* *
* @param bool $showAll Include all of the elements, even those not shown in the menus. Only * @param bool $showAll Include all of the elements, even those not shown in the menus. Only
* applicable when extension is applied to {@link SiteTree}. * applicable when extension is applied to {@link SiteTree}.
* @param bool $onlyDeletedFromStage Only return items that have been deleted from stage * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage
* @return DataList * @return DataList
* @throws Exception * @throws Exception
*/ */
public function liveChildren($showAll = false, $onlyDeletedFromStage = false) public function liveChildren($showAll = false, $onlyDeletedFromStage = false)
{ {
if(!$this->owner->hasExtension(Versioned::class)) { if (!$this->owner->hasExtension(Versioned::class)) {
throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
} }
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy; $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree; $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
$children = $baseClass::get() $children = $baseClass::get()
->filter('ParentID', (int)$this->owner->ID) ->filter('ParentID', (int)$this->owner->ID)
->exclude('ID', (int)$this->owner->ID) ->exclude('ID', (int)$this->owner->ID)
->setDataQueryParam(array( ->setDataQueryParam(array(
'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage', 'Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage',
'Versioned.stage' => 'Live' 'Versioned.stage' => 'Live'
)); ));
if ($hide_from_hierarchy) { if ($hide_from_hierarchy) {
$children = $children->exclude('ClassName', $hide_from_hierarchy); $children = $children->exclude('ClassName', $hide_from_hierarchy);
} }
if ($hide_from_cms_tree && $this->showingCMSTree()) { if ($hide_from_cms_tree && $this->showingCMSTree()) {
$children = $children->exclude('ClassName', $hide_from_cms_tree); $children = $children->exclude('ClassName', $hide_from_cms_tree);
} }
if(!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) {
$children = $children->filter('ShowInMenus', 1); $children = $children->filter('ShowInMenus', 1);
} }
return $children; return $children;
} }
/** /**
* Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing * Get this object's parent, optionally filtered by an SQL clause. If the clause doesn't match the parent, nothing
* is returned. * is returned.
* *
* @param string $filter * @param string $filter
* @return DataObject * @return DataObject
*/ */
public function getParent($filter = null) public function getParent($filter = null)
{ {
$parentID = $this->owner->ParentID; $parentID = $this->owner->ParentID;
if(empty($parentID)) { if (empty($parentID)) {
return null; return null;
} }
$idSQL = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ID'); $idSQL = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ID');
return DataObject::get_one($this->owner->class, array( return DataObject::get_one($this->owner->class, array(
array($idSQL => $parentID), array($idSQL => $parentID),
$filter $filter
)); ));
} }
/** /**
* Return all the parents of this class in a set ordered from the lowest to highest parent. * Return all the parents of this class in a set ordered from the lowest to highest parent.
* *
* @return ArrayList * @return ArrayList
*/ */
public function getAncestors() public function getAncestors()
{ {
$ancestors = new ArrayList(); $ancestors = new ArrayList();
$object = $this->owner; $object = $this->owner;
while($object = $object->getParent()) { while ($object = $object->getParent()) {
$ancestors->push($object); $ancestors->push($object);
} }
return $ancestors; return $ancestors;
} }
/** /**
* Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute. * Returns a human-readable, flattened representation of the path to the object, using its {@link Title} attribute.
* *
* @param string $separator * @param string $separator
* @return string * @return string
*/ */
public function getBreadcrumbs($separator = ' &raquo; ') public function getBreadcrumbs($separator = ' &raquo; ')
{ {
$crumbs = array(); $crumbs = array();
$ancestors = array_reverse($this->owner->getAncestors()->toArray()); $ancestors = array_reverse($this->owner->getAncestors()->toArray());
foreach ($ancestors as $ancestor) { foreach ($ancestors as $ancestor) {
$crumbs[] = $ancestor->Title; $crumbs[] = $ancestor->Title;
} }
$crumbs[] = $this->owner->Title; $crumbs[] = $this->owner->Title;
return implode($separator, $crumbs); return implode($separator, $crumbs);
} }
/** /**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node, * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents. * then search the parents.
* *
* @todo Write! * @todo Write!
* *
* @param string $className Class name of the node to find * @param string $className Class name of the node to find
* @param DataObject $afterNode Used for recursive calls to this function * @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject * @return DataObject
*/ */
public function naturalPrev($className, $afterNode = null) public function naturalPrev($className, $afterNode = null)
{ {
return null; return null;
} }
/** /**
* Get the next node in the tree of the type. If there is no instance of the className descended from this node, * Get the next node in the tree of the type. If there is no instance of the className descended from this node,
* then search the parents. * then search the parents.
* @param string $className Class name of the node to find. * @param string $className Class name of the node to find.
* @param string|int $root ID/ClassName of the node to limit the search to * @param string|int $root ID/ClassName of the node to limit the search to
* @param DataObject $afterNode Used for recursive calls to this function * @param DataObject $afterNode Used for recursive calls to this function
* @return DataObject * @return DataObject
*/ */
public function naturalNext($className = null, $root = 0, $afterNode = null) public function naturalNext($className = null, $root = 0, $afterNode = null)
{ {
// If this node is not the node we are searching from, then we can possibly return this node as a solution // If this node is not the node we are searching from, then we can possibly return this node as a solution
if($afterNode && $afterNode->ID != $this->owner->ID) { if ($afterNode && $afterNode->ID != $this->owner->ID) {
if(!$className || ($className && $this->owner->class == $className)) { if (!$className || ($className && $this->owner->class == $className)) {
return $this->owner; return $this->owner;
} }
} }
$nextNode = null; $nextNode = null;
$baseClass = $this->owner->baseClass(); $baseClass = $this->owner->baseClass();
$children = $baseClass::get() $children = $baseClass::get()
->filter('ParentID', (int)$this->owner->ID) ->filter('ParentID', (int)$this->owner->ID)
->sort('"Sort"', 'ASC'); ->sort('"Sort"', 'ASC');
if ($afterNode) { if ($afterNode) {
$children = $children->filter('Sort:GreaterThan', $afterNode->Sort); $children = $children->filter('Sort:GreaterThan', $afterNode->Sort);
} }
// Try all the siblings of this node after the given node // Try all the siblings of this node after the given node
/*if( $siblings = DataObject::get( $this->owner->baseClass(), /*if( $siblings = DataObject::get( $this->owner->baseClass(),
"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\" "\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/ > {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
if($children) { if ($children) {
foreach($children as $node) { foreach ($children as $node) {
if($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) { if ($nextNode = $node->naturalNext($className, $node->ID, $this->owner)) {
break; break;
} }
} }
if($nextNode) { if ($nextNode) {
return $nextNode; return $nextNode;
} }
} }
// if this is not an instance of the root class or has the root id, search the parent // if this is not an instance of the root class or has the root id, search the parent
if(!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class) if (!(is_numeric($root) && $root == $this->owner->ID || $root == $this->owner->class)
&& ($parent = $this->owner->Parent())) { && ($parent = $this->owner->Parent())) {
return $parent->naturalNext( $className, $root, $this->owner ); return $parent->naturalNext($className, $root, $this->owner);
} }
return null; return null;
} }
/** /**
* Flush all Hierarchy caches: * Flush all Hierarchy caches:
* - Children (instance) * - Children (instance)
* - NumChildren (instance) * - NumChildren (instance)
* - Marked (global) * - Marked (global)
* - Expanded (global) * - Expanded (global)
* - TreeOpened (global) * - TreeOpened (global)
*/ */
public function flushCache() public function flushCache()
{ {
$this->_cache_children = null; $this->_cache_children = null;
$this->_cache_numChildren = null; $this->_cache_numChildren = null;
self::$marked = array(); self::$marked = array();
self::$expanded = array(); self::$expanded = array();
self::$treeOpened = array(); self::$treeOpened = array();
} }
/** /**
* Reset global Hierarchy caches: * Reset global Hierarchy caches:
* - Marked * - Marked
* - Expanded * - Expanded
* - TreeOpened * - TreeOpened
*/ */
public static function reset() public static function reset()
{ {
self::$marked = array(); self::$marked = array();
self::$expanded = array(); self::$expanded = array();
self::$treeOpened = array(); self::$treeOpened = array();
} }
} }

View File

@ -3,6 +3,8 @@
namespace SilverStripe\ORM; namespace SilverStripe\ORM;
use Exception; use Exception;
use InvalidArgumentException;
use SilverStripe\Core\Injector\Injectable;
/** /**
* Exception thrown by {@link DataObject}::write if validation fails. By throwing an * Exception thrown by {@link DataObject}::write if validation fails. By throwing an
@ -11,73 +13,62 @@ use Exception;
*/ */
class ValidationException extends Exception class ValidationException extends Exception
{ {
use Injectable;
/** /**
* The contained ValidationResult related to this error * The contained ValidationResult related to this error
* *
* @var ValidationResult * @var ValidationResult
*/ */
protected $result; protected $result;
/** /**
* Construct a new ValidationException with an optional ValidationResult object * Construct a new ValidationException with an optional ValidationResult object
* *
* @param ValidationResult|string $result The ValidationResult containing the * @param ValidationResult|string $result The ValidationResult containing the
* failed result. Can be substituted with an error message instead if no * failed result, or error message to build error from
* ValidationResult exists. * @param integer $code The error code number
* @param string|integer $message The error message. If $result was given the */
* message string rather than a ValidationResult object then this will have public function __construct($result = null, $code = 0)
* the error code number. {
* @param integer $code The error code number, if not given in the second parameter // Catch legacy behaviour where second argument was not code
*/ if ($code && !is_numeric($code)) {
public function __construct($result = null, $code = 0, $dummy = null) { throw new InvalidArgumentException("Code must be numeric");
$exceptionMessage = null; }
// Backwards compatibiliy failover. The 2nd argument used to be $message, and $code the 3rd. // Set default message and result
// For callers using that, we ditch the message $exceptionMessage = _t("ValidationException.DEFAULT_ERROR", "Validation error");
if(!is_numeric($code)) { if (!$result) {
$exceptionMessage = $code; $result = $exceptionMessage;
if($dummy) $code = $dummy; }
}
if($result instanceof ValidationResult) { // Check result type
$this->result = $result; if ($result instanceof ValidationResult) {
$this->result = $result;
// Pick first message
foreach ($result->getMessages() as $message) {
$exceptionMessage = $message['message'];
break;
}
} elseif (is_string($result)) {
$this->result = ValidationResult::create()->addError($result);
$exceptionMessage = $result;
} else {
throw new InvalidArgumentException(
"ValidationExceptions must be passed a ValdiationResult, a string, or nothing at all"
);
}
} else if(is_string($result)) { parent::__construct($exceptionMessage, $code);
$this->result = ValidationResult::create()->addError($result); }
} else if(!$result) { /**
$this->result = ValidationResult::create()->addError(_t("ValdiationExcetpion.DEFAULT_ERROR", "Validation error")); * Retrieves the ValidationResult related to this error
*
} else { * @return ValidationResult
throw new InvalidArgumentException( */
"ValidationExceptions must be passed a ValdiationResult, a string, or nothing at all");
}
// Construct
parent::__construct($exceptionMessage ? $exceptionMessage : $this->result->message(), $code);
}
/**
* Create a ValidationException with a message for a single field-specific error message.
*
* @param string $field The field name
* @param string $message The error message
* @return ValidationException
*/
static function create_for_field($field, $message) {
$result = new ValidationResult;
$result->addFieldError($field, $message);
return new ValidationException($result);
}
/**
* Retrieves the ValidationResult related to this error
*
* @return ValidationResult
*/
public function getResult() public function getResult()
{ {
return $this->result; return $this->result;
} }
} }

View File

@ -2,259 +2,235 @@
namespace SilverStripe\ORM; namespace SilverStripe\ORM;
use SilverStripe\Core\Object; use InvalidArgumentException;
use Serializable;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\Deprecation;
/** /**
* A class that combined as a boolean result with an optional list of error messages. * A class that combined as a boolean result with an optional list of error messages.
* This is used for returning validation results from validators * This is used for returning validation results from validators
*
* Each message can have a code or field which will uniquely identify that message. However,
* messages can be stored without a field or message as an "overall" message.
*/ */
class ValidationResult extends Object class ValidationResult implements Serializable
{ {
/** use Injectable;
* @var bool - is the result valid or not
*/
protected $isValid = true;
/**
* Standard "error" type
*/
const TYPE_ERROR = 'error';
/** /**
* @var array of errors * Standard "good" message type
*/ */
protected $errorList = array(); const TYPE_GOOD = 'good';
/** /**
* Create a new ValidationResult. * Non-error message type.
* By default, it is a successful result. Call $this->error() to record errors. */
* const TYPE_INFO = 'info';
* @param void $valid @deprecated
* @param void $message @deprecated /**
*/ * Warning message type
public function __construct($valid = null, $message = null) { */
if ($message !== null) { const TYPE_WARNING = 'warning';
Deprecation::notice('3.2', '$message parameter is deprecated please use addMessage or addError instead', false);
$this->addError($message); /**
} * Message type is html
if ($valid !== null) { */
Deprecation::notice('3.2', '$valid parameter is deprecated please addError to mark the result as invalid', false); const CAST_HTML = 'html';
$this->isValid = $valid;
if ($message) { /**
$this->errorList[] = $message; * Message type is plain text
*/
const CAST_TEXT = 'text';
/**
* Is the result valid or not.
* Note that there can be non-error messages in the list.
*
* @var bool
*/
protected $isValid = true;
/**
* List of messages
*
* @var array
*/
protected $messages = array();
/**
* Create a new ValidationResult.
* By default, it is a successful result. Call $this->error() to record errors.
*/
public function __construct()
{
if (func_num_args() > 0) {
Deprecation::notice('3.2', '$valid parameter is deprecated please addError to mark the result as invalid', false);
$this->isValid = func_get_arg(0);
} }
parent::__construct(); if (func_num_args() > 1) {
} Deprecation::notice('3.2', '$message parameter is deprecated please use addMessage or addError instead', false);
$this->addError(func_get_arg(1));
/**
* Return the full error meta-data, suitable for combining with another ValidationResult.
*/
function getErrorMetaData() {
return $this->errorList;
}
/**
* Record a
* against this validation result.
*
* It's better to use addError, addFeildError, addMessage, or addFieldMessage instead.
*
* @param string $message The message string.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param string $fieldName The field to link the message to. If omitted; a form-wide message is assumed.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param bool $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
*
* @deprecated 3.2
*/
public function error($message, $code = null, $fieldName = null, $messageType = "bad", $escapeHtml = true) {
Deprecation::notice('3.2', 'Use addError or addFieldError instead.');
return $this->addFieldError($fieldName, $message, $messageType, $code, $escapeHtml);
}
/**
* Record an error against this validation result,
*
* @param string $message The message string.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param bool $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
*/
public function addError($message, $messageType = "bad", $code = null, $escapeHtml = true) {
return $this->addFieldError(null, $message, $messageType, $code, $escapeHtml);
}
/**
* Record an error against this validation result,
*
* @param string $fieldName The field to link the message to. If omitted; a form-wide message is assumed.
* @param string $message The message string.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param bool $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
*/
public function addFieldError($fieldName = null, $message, $messageType = "bad", $code = null, $escapeHtml = true) {
$this->isValid = false;
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $escapeHtml);
}
/**
* Add a message to this ValidationResult without necessarily marking it as an error
*
* @param string $message The message string.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param bool $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
*/
public function addMessage($message, $messageType = "bad", $code = null, $escapeHtml = true) {
return $this->addFieldMessage(null, $message, $messageType, $code, $escapeHtml);
}
/**
* Add a message to this ValidationResult without necessarily marking it as an error
*
* @param string $fieldName The field to link the message to. If omitted; a form-wide message is assumed.
* @param string $message The message string.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param bool $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML.
* In that case, you might want to use {@link Convert::raw2xml()} to escape any
* user supplied data in the message.
*/
public function addFieldMessage($fieldName, $message, $messageType = "bad", $code = null, $escapeHtml = true) {
$metadata = array(
'message' => $escapeHtml ? Convert::raw2xml($message) : $message,
'fieldName' => $fieldName,
'messageType' => $messageType,
);
if($code) {
if(!is_numeric($code)) {
$this->errorList[$code] = $metadata;
} else {
throw new InvalidArgumentException(
"ValidationResult::error() - Don't use a numeric code '$code'. Use a string.");
}
} else {
$this->errorList[] = $metadata;
}
return $this;
}
/**
* Returns true if the result is valid.
* @return boolean
*/
public function valid()
{
return $this->isValid;
}
/**
* Get an array of errors
* @return array
*/
public function messageList()
{
$list = array();
foreach($this->errorList as $key => $item) {
if(is_numeric($key)) $list[] = $item['message'];
else $list[$key] = $item['message'];
}
return $list;
}
/**
* Get the field-specific messages as a map.
* Keys will be field names, and values will be a 2 element map with keys 'messsage', and 'messageType'
*/
public function fieldErrors() {
$output = array();
foreach($this->errorList as $key => $item) {
if($item['fieldName']) {
$output[$item['fieldName']] = array(
'message' => $item['message'],
'messageType' => $item['messageType']
);
}
}
return $output;
}
/**
* Get an array of error codes
* @return array
*/
public function codeList()
{
$codeList = array();
foreach ($this->errorList as $k => $v) {
if (!is_numeric($k)) {
$codeList[] = $k;
}
} }
return $codeList; }
}
/** /**
* Get the error message as a string. * Record an error against this validation result,
* @return string *
*/ * @param string $message The message string.
public function message() * @param string $messageType Passed as a CSS class to the form, so other values can be used if desired.
* Standard types are defined by the TYPE_ constant definitions.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
* @return $this
*/
public function addError($message, $messageType = self::TYPE_ERROR, $code = null, $cast = self::CAST_TEXT)
{ {
return implode("; ", $this->messageList()); return $this->addFieldError(null, $message, $messageType, $code, $cast);
} }
/** /**
* The the error message that's not related to a field as a string * Record an error against this validation result,
*/ *
public function overallMessage() { * @param string $fieldName The field to link the message to. If omitted; a form-wide message is assumed.
$messages = array(); * @param string $message The message string.
foreach($this->errorList as $item) { * @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
if(!$item['fieldName']) $messages[] = $item['message']; * class to the form, so other values can be used if desired.
} * @param string $code A codename for this error. Only one message per codename will be added.
return implode("; ", $messages); * This can be usedful for ensuring no duplicate messages
} * @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
* @return $this
*/
public function addFieldError(
$fieldName,
$message,
$messageType = self::TYPE_ERROR,
$code = null,
$cast = self::CAST_TEXT
) {
$this->isValid = false;
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast);
}
/** /**
* Get a starred list of all messages * Add a message to this ValidationResult without necessarily marking it as an error
* @return string *
*/ * @param string $message The message string.
public function starredList() * @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* class to the form, so other values can be used if desired.
* @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
* @return $this
*/
public function addMessage($message, $messageType = self::TYPE_ERROR, $code = null, $cast = self::CAST_TEXT)
{ {
return " * " . implode("\n * ", $this->messageList()); return $this->addFieldMessage(null, $message, $messageType, $code, $cast);
} }
/** /**
* Combine this Validation Result with the ValidationResult given in other. * Add a message to this ValidationResult without necessarily marking it as an error
* It will be valid if both this and the other result are valid. *
* This object will be modified to contain the new validation information. * @param string $fieldName The field to link the message to. If omitted; a form-wide message is assumed.
* * @param string $message The message string.
* @param ValidationResult $other the validation result object to combine * @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS
* @return $this * class to the form, so other values can be used if desired.
*/ * @param string $code A codename for this error. Only one message per codename will be added.
* This can be usedful for ensuring no duplicate messages
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
* @return $this
*/
public function addFieldMessage(
$fieldName,
$message,
$messageType = self::TYPE_ERROR,
$code = null,
$cast = self::CAST_TEXT
) {
if ($code && is_numeric($code)) {
throw new InvalidArgumentException("Don't use a numeric code '$code'. Use a string.");
}
if (is_bool($cast)) {
$cast = $cast ? self::CAST_TEXT : self::CAST_HTML;
}
$metadata = array(
'message' => $message,
'fieldName' => $fieldName,
'messageType' => $messageType,
'messageCast' => $cast,
);
if ($code) {
$this->messages[$code] = $metadata;
} else {
$this->messages[] = $metadata;
}
return $this;
}
/**
* Returns true if the result is valid.
* @return boolean
*/
public function isValid()
{
return $this->isValid;
}
/**
* Return the full error meta-data, suitable for combining with another ValidationResult.
*
* @return array Array of messages, where each item is an array of data for that message.
*/
public function getMessages()
{
return $this->messages;
}
/**
* Combine this Validation Result with the ValidationResult given in other.
* It will be valid if both this and the other result are valid.
* This object will be modified to contain the new validation information.
*
* @param ValidationResult $other the validation result object to combine
* @return $this
*/
public function combineAnd(ValidationResult $other) public function combineAnd(ValidationResult $other)
{ {
$this->isValid = $this->isValid && $other->valid(); $this->isValid = $this->isValid && $other->isValid();
$this->errorList = array_merge($this->errorList, $other->getErrorMetaData()); $this->messages = array_merge($this->messages, $other->getMessages());
return $this; return $this;
} }
/**
* String representation of object
*
* @return string the string representation of the object or null
*/
public function serialize()
{
return json_encode([$this->messages, $this->isValid]);
}
/**
* Constructs the object
*
* @param string $serialized
*/
public function unserialize($serialized)
{
list($this->messages, $this->isValid) = json_decode($serialized, true);
}
} }

View File

@ -8,7 +8,7 @@ use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationResult;
/** /**
* Provides versioned dataobject support to {@see GridFieldDetailForm_ItemRequest} * Provides versioned dataobject support to {@see GridFieldDetailForm_ItemRequest}
@ -25,7 +25,7 @@ class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
// Check if record is versionable // Check if record is versionable
/** @var Versioned|DataObject $record */ /** @var Versioned|DataObject $record */
$record = $this->getRecord(); $record = $this->getRecord();
if (!$record || !$record->has_extension('SilverStripe\ORM\Versioning\Versioned')) { if (!$record || !$record->has_extension(Versioned::class)) {
return $actions; return $actions;
} }
@ -100,12 +100,7 @@ class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
// Record name before it's deleted // Record name before it's deleted
$title = $record->Title; $title = $record->Title;
try {
$record->doArchive(); $record->doArchive();
} catch (ValidationException $e) {
return $this->generateValidationResponse($form, $e);
}
$message = sprintf( $message = sprintf(
_t('VersionedGridFieldItemRequest.Archived', 'Archived %s %s'), _t('VersionedGridFieldItemRequest.Archived', 'Archived %s %s'),
@ -139,15 +134,9 @@ class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
return $this->httpError(403); return $this->httpError(403);
} }
// Save from form data
try {
// Initial save and reload // Initial save and reload
$record = $this->saveFormIntoRecord($data, $form); $record = $this->saveFormIntoRecord($data, $form);
$record->publishRecursive(); $record->publishRecursive();
} catch (ValidationException $e) {
return $this->generateValidationResponse($form, $e);
}
$editURL = $this->Link('edit'); $editURL = $this->Link('edit');
$xmlTitle = Convert::raw2xml($record->Title); $xmlTitle = Convert::raw2xml($record->Title);
$link = "<a href=\"{$editURL}\">{$xmlTitle}</a>"; $link = "<a href=\"{$editURL}\">{$xmlTitle}</a>";
@ -181,12 +170,7 @@ class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
// Record name before it's deleted // Record name before it's deleted
$title = $record->Title; $title = $record->Title;
try {
$record->doUnpublish(); $record->doUnpublish();
} catch (ValidationException $e) {
return $this->generateValidationResponse($form, $e);
}
$message = sprintf( $message = sprintf(
_t('VersionedGridFieldItemRequest.Unpublished', 'Unpublished %s %s'), _t('VersionedGridFieldItemRequest.Unpublished', 'Unpublished %s %s'),
@ -205,11 +189,12 @@ class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
*/ */
protected function setFormMessage($form, $message) protected function setFormMessage($form, $message)
{ {
$form->sessionMessage($message, 'good', false); $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
$controller = $this->getToplevelController(); $controller = $this->getToplevelController();
if ($controller->hasMethod('getEditForm')) { if ($controller->hasMethod('getEditForm')) {
/** @var Form $backForm */
$backForm = $controller->getEditForm(); $backForm = $controller->getEditForm();
$backForm->sessionMessage($message, 'good', false); $backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
} }
} }
} }

View File

@ -124,12 +124,13 @@ class CMSMemberLoginForm extends LoginForm
/** /**
* Redirect the user to the change password form. * Redirect the user to the change password form.
* *
* @skipUpgrade
* @return HTTPResponse * @return HTTPResponse
*/ */
protected function redirectToChangePassword() protected function redirectToChangePassword()
{ {
// Since this form is loaded via an iframe, this redirect must be performed via javascript // Since this form is loaded via an iframe, this redirect must be performed via javascript
$changePasswordForm = new ChangePasswordForm($this->controller, 'SilverStripe\\Security\\ChangePasswordForm'); $changePasswordForm = new ChangePasswordForm($this->controller, 'ChangePasswordForm');
$changePasswordForm->sessionMessage( $changePasswordForm->sessionMessage(
_t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'), _t('Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good' 'good'

View File

@ -14,6 +14,7 @@ use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationResult;
/** /**
* Standard Change Password Form * Standard Change Password Form
@ -64,7 +65,6 @@ class ChangePasswordForm extends Form
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $name, $fields, $actions);
} }
/** /**
* Change the password * Change the password
* *
@ -75,7 +75,7 @@ class ChangePasswordForm extends Form
{ {
if ($member = Member::currentUser()) { if ($member = Member::currentUser()) {
// The user was logged in, check the current password // The user was logged in, check the current password
if (empty($data['OldPassword']) || !$member->checkPassword($data['OldPassword'])->valid()) { if (empty($data['OldPassword']) || !$member->checkPassword($data['OldPassword'])->isValid()) {
$this->clearMessage(); $this->clearMessage();
$this->sessionMessage( $this->sessionMessage(
_t('Member.ERRORPASSWORDNOTMATCH', "Your current password does not match, please try again"), _t('Member.ERRORPASSWORDNOTMATCH', "Your current password does not match, please try again"),
@ -108,60 +108,52 @@ class ChangePasswordForm extends Form
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere. // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword')); return $this->controller->redirect($this->controller->Link('changepassword'));
} elseif ($data['NewPassword1'] == $data['NewPassword2']) { }
$isValid = $member->changePassword($data['NewPassword1']);
if ($isValid->valid()) {
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if ($member->canLogIn()->valid()) { // Fail if passwords do not match
$member->logIn(); if ($data['NewPassword1'] !== $data['NewPassword2']) {
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
if (!empty($_REQUEST['BackURL'])
// absolute redirection URLs may cause spoofing
&& Director::is_site_url($_REQUEST['BackURL'])
) {
$url = Director::absoluteURL($_REQUEST['BackURL']);
return $this->controller->redirect($url);
} else {
// Redirect to default location - the login form saying "You are logged in as..."
$redirectURL = HTTP::setGetVar(
'BackURL',
Director::absoluteBaseURL(),
$this->controller->Link('login')
);
return $this->controller->redirect($redirectURL);
}
} else {
$this->clearMessage();
$this->sessionMessage(
_t(
'Member.INVALIDNEWPASSWORD',
"We couldn't accept that password: {password}",
array('password' => nl2br("\n".Convert::raw2xml($isValid->starredList())))
),
"bad",
false
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
} else {
$this->clearMessage(); $this->clearMessage();
$this->sessionMessage( $this->sessionMessage(
_t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"), _t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad" "bad"
); );
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere. // redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword')); return $this->controller->redirect($this->controller->Link('changepassword'));
} }
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$this->setSessionValidationResult($validationResult);
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if ($member->canLogIn()->isValid()) {
$member->logIn();
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
if (!empty($_REQUEST['BackURL'])
// absolute redirection URLs may cause spoofing
&& Director::is_site_url($_REQUEST['BackURL'])
) {
$url = Director::absoluteURL($_REQUEST['BackURL']);
return $this->controller->redirect($url);
} else {
// Redirect to default location - the login form saying "You are logged in as..."
$redirectURL = HTTP::setGetVar(
'BackURL',
Director::absoluteBaseURL(),
$this->controller->Link('login')
);
return $this->controller->redirect($redirectURL);
}
} }
} }

View File

@ -53,436 +53,436 @@ use SilverStripe\View\Requirements;
class Group extends DataObject class Group extends DataObject
{ {
private static $db = array( private static $db = array(
"Title" => "Varchar(255)", "Title" => "Varchar(255)",
"Description" => "Text", "Description" => "Text",
"Code" => "Varchar(255)", "Code" => "Varchar(255)",
"Locked" => "Boolean", "Locked" => "Boolean",
"Sort" => "Int", "Sort" => "Int",
"HtmlEditorConfig" => "Text" "HtmlEditorConfig" => "Text"
); );
private static $has_one = array( private static $has_one = array(
"Parent" => "SilverStripe\\Security\\Group", "Parent" => "SilverStripe\\Security\\Group",
); );
private static $has_many = array( private static $has_many = array(
"Permissions" => "SilverStripe\\Security\\Permission", "Permissions" => "SilverStripe\\Security\\Permission",
"Groups" => "SilverStripe\\Security\\Group" "Groups" => "SilverStripe\\Security\\Group"
); );
private static $many_many = array( private static $many_many = array(
"Members" => "SilverStripe\\Security\\Member", "Members" => "SilverStripe\\Security\\Member",
"Roles" => "SilverStripe\\Security\\PermissionRole", "Roles" => "SilverStripe\\Security\\PermissionRole",
); );
private static $extensions = array( private static $extensions = array(
"SilverStripe\\ORM\\Hierarchy\\Hierarchy", "SilverStripe\\ORM\\Hierarchy\\Hierarchy",
); );
private static $table_name = "Group"; private static $table_name = "Group";
public function populateDefaults() public function populateDefaults()
{ {
parent::populateDefaults(); parent::populateDefaults();
if (!$this->Title) { if (!$this->Title) {
$this->Title = _t('SecurityAdmin.NEWGROUP', "New Group"); $this->Title = _t('SecurityAdmin.NEWGROUP', "New Group");
} }
} }
public function getAllChildren() public function getAllChildren()
{ {
$doSet = new ArrayList(); $doSet = new ArrayList();
$children = Group::get()->filter("ParentID", $this->ID); $children = Group::get()->filter("ParentID", $this->ID);
foreach($children as $child) { foreach ($children as $child) {
$doSet->push($child); $doSet->push($child);
$doSet->merge($child->getAllChildren()); $doSet->merge($child->getAllChildren());
} }
return $doSet; return $doSet;
} }
/** /**
* Caution: Only call on instances, not through a singleton. * Caution: Only call on instances, not through a singleton.
* The "root group" fields will be created through {@link SecurityAdmin->EditForm()}. * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
* *
* @return FieldList * @return FieldList
*/ */
public function getCMSFields() public function getCMSFields()
{ {
$fields = new FieldList( $fields = new FieldList(
new TabSet( new TabSet(
"Root", "Root",
new Tab( new Tab(
'Members', 'Members',
_t('SecurityAdmin.MEMBERS', 'Members'), _t('SecurityAdmin.MEMBERS', 'Members'),
new TextField("Title", $this->fieldLabel('Title')), new TextField("Title", $this->fieldLabel('Title')),
$parentidfield = DropdownField::create( $parentidfield = DropdownField::create(
'ParentID', 'ParentID',
$this->fieldLabel('Parent'), $this->fieldLabel('Parent'),
Group::get()->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs') Group::get()->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs')
)->setEmptyString(' '), )->setEmptyString(' '),
new TextareaField('Description', $this->fieldLabel('Description')) new TextareaField('Description', $this->fieldLabel('Description'))
), ),
$permissionsTab = new Tab( $permissionsTab = new Tab(
'Permissions', 'Permissions',
_t('SecurityAdmin.PERMISSIONS', 'Permissions'), _t('SecurityAdmin.PERMISSIONS', 'Permissions'),
$permissionsField = new PermissionCheckboxSetField( $permissionsField = new PermissionCheckboxSetField(
'Permissions', 'Permissions',
false, false,
'SilverStripe\\Security\\Permission', 'SilverStripe\\Security\\Permission',
'GroupID', 'GroupID',
$this $this
) )
) )
) )
); );
$parentidfield->setDescription( $parentidfield->setDescription(
_t('Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles') _t('Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles')
); );
// Filter permissions // Filter permissions
// TODO SecurityAdmin coupling, not easy to get to the form fields through GridFieldDetailForm // TODO SecurityAdmin coupling, not easy to get to the form fields through GridFieldDetailForm
$permissionsField->setHiddenPermissions((array)Config::inst()->get('SilverStripe\\Admin\\SecurityAdmin', 'hidden_permissions')); $permissionsField->setHiddenPermissions((array)Config::inst()->get('SilverStripe\\Admin\\SecurityAdmin', 'hidden_permissions'));
if($this->ID) { if ($this->ID) {
$group = $this; $group = $this;
$config = GridFieldConfig_RelationEditor::create(); $config = GridFieldConfig_RelationEditor::create();
$config->addComponent(new GridFieldButtonRow('after')); $config->addComponent(new GridFieldButtonRow('after'));
$config->addComponents(new GridFieldExportButton('buttons-after-left')); $config->addComponents(new GridFieldExportButton('buttons-after-left'));
$config->addComponents(new GridFieldPrintButton('buttons-after-left')); $config->addComponents(new GridFieldPrintButton('buttons-after-left'));
/** @var GridFieldAddExistingAutocompleter $autocompleter */ /** @var GridFieldAddExistingAutocompleter $autocompleter */
$autocompleter = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldAddExistingAutocompleter'); $autocompleter = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldAddExistingAutocompleter');
/** @skipUpgrade */ /** @skipUpgrade */
$autocompleter $autocompleter
->setResultsFormat('$Title ($Email)') ->setResultsFormat('$Title ($Email)')
->setSearchFields(array('FirstName', 'Surname', 'Email')); ->setSearchFields(array('FirstName', 'Surname', 'Email'));
/** @var GridFieldDetailForm $detailForm */ /** @var GridFieldDetailForm $detailForm */
$detailForm = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDetailForm'); $detailForm = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDetailForm');
$detailForm $detailForm
->setValidator(Member_Validator::create()) ->setValidator(Member_Validator::create())
->setItemEditFormCallback(function($form, $component) use($group) { ->setItemEditFormCallback(function ($form, $component) use ($group) {
/** @var Form $form */ /** @var Form $form */
$record = $form->getRecord(); $record = $form->getRecord();
$groupsField = $form->Fields()->dataFieldByName('DirectGroups'); $groupsField = $form->Fields()->dataFieldByName('DirectGroups');
if($groupsField) { if ($groupsField) {
// If new records are created in a group context, // If new records are created in a group context,
// set this group by default. // set this group by default.
if($record && !$record->ID) { if ($record && !$record->ID) {
$groupsField->setValue($group->ID); $groupsField->setValue($group->ID);
} elseif($record && $record->ID) { } elseif ($record && $record->ID) {
// TODO Mark disabled once chosen.js supports it // TODO Mark disabled once chosen.js supports it
// $groupsField->setDisabledItems(array($group->ID)); // $groupsField->setDisabledItems(array($group->ID));
$form->Fields()->replaceField( $form->Fields()->replaceField(
'DirectGroups', 'DirectGroups',
$groupsField->performReadonlyTransformation() $groupsField->performReadonlyTransformation()
); );
} }
} }
}); });
$memberList = GridField::create('Members',false, $this->DirectMembers(), $config) $memberList = GridField::create('Members', false, $this->DirectMembers(), $config)
->addExtraClass('members_grid'); ->addExtraClass('members_grid');
// @todo Implement permission checking on GridField // @todo Implement permission checking on GridField
//$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
$fields->addFieldToTab('Root.Members', $memberList); $fields->addFieldToTab('Root.Members', $memberList);
} }
// Only add a dropdown for HTML editor configurations if more than one is available. // Only add a dropdown for HTML editor configurations if more than one is available.
// Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration. // Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
$editorConfigMap = HTMLEditorConfig::get_available_configs_map(); $editorConfigMap = HTMLEditorConfig::get_available_configs_map();
if(count($editorConfigMap) > 1) { if (count($editorConfigMap) > 1) {
$fields->addFieldToTab( $fields->addFieldToTab(
'Root.Permissions', 'Root.Permissions',
new DropdownField( new DropdownField(
'HtmlEditorConfig', 'HtmlEditorConfig',
'HTML Editor Configuration', 'HTML Editor Configuration',
$editorConfigMap $editorConfigMap
), ),
'Permissions' 'Permissions'
); );
} }
if(!Permission::check('EDIT_PERMISSIONS')) { if (!Permission::check('EDIT_PERMISSIONS')) {
$fields->removeFieldFromTab('Root', 'Permissions'); $fields->removeFieldFromTab('Root', 'Permissions');
} }
// Only show the "Roles" tab if permissions are granted to edit them, // Only show the "Roles" tab if permissions are granted to edit them,
// and at least one role exists // and at least one role exists
if(Permission::check('APPLY_ROLES') && DataObject::get('SilverStripe\\Security\\PermissionRole')) { if (Permission::check('APPLY_ROLES') && DataObject::get('SilverStripe\\Security\\PermissionRole')) {
$fields->findOrMakeTab('Root.Roles', _t('SecurityAdmin.ROLES', 'Roles')); $fields->findOrMakeTab('Root.Roles', _t('SecurityAdmin.ROLES', 'Roles'));
$fields->addFieldToTab( $fields->addFieldToTab(
'Root.Roles', 'Root.Roles',
new LiteralField( new LiteralField(
"", "",
"<p>" . "<p>" .
_t( _t(
'SecurityAdmin.ROLESDESCRIPTION', 'SecurityAdmin.ROLESDESCRIPTION',
"Roles are predefined sets of permissions, and can be assigned to groups.<br />" "Roles are predefined sets of permissions, and can be assigned to groups.<br />"
. "They are inherited from parent groups if required." . "They are inherited from parent groups if required."
) . '<br />' . ) . '<br />' .
sprintf( sprintf(
'<a href="%s" class="add-role">%s</a>', '<a href="%s" class="add-role">%s</a>',
SecurityAdmin::singleton()->Link('show/root#Root_Roles'), SecurityAdmin::singleton()->Link('show/root#Root_Roles'),
// TODO This should include #Root_Roles to switch directly to the tab, // TODO This should include #Root_Roles to switch directly to the tab,
// but tabstrip.js doesn't display tabs when directly adressed through a URL pragma // but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
_t('Group.RolesAddEditLink', 'Manage roles') _t('Group.RolesAddEditLink', 'Manage roles')
) . ) .
"</p>" "</p>"
) )
); );
// Add roles (and disable all checkboxes for inherited roles) // Add roles (and disable all checkboxes for inherited roles)
$allRoles = PermissionRole::get(); $allRoles = PermissionRole::get();
if(!Permission::check('ADMIN')) { if (!Permission::check('ADMIN')) {
$allRoles = $allRoles->filter("OnlyAdminCanApply", 0); $allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
} }
if($this->ID) { if ($this->ID) {
$groupRoles = $this->Roles(); $groupRoles = $this->Roles();
$inheritedRoles = new ArrayList(); $inheritedRoles = new ArrayList();
$ancestors = $this->getAncestors(); $ancestors = $this->getAncestors();
foreach($ancestors as $ancestor) { foreach ($ancestors as $ancestor) {
$ancestorRoles = $ancestor->Roles(); $ancestorRoles = $ancestor->Roles();
if ($ancestorRoles) { if ($ancestorRoles) {
$inheritedRoles->merge($ancestorRoles); $inheritedRoles->merge($ancestorRoles);
} }
} }
$groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID'); $groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID');
$inheritedRoleIDs = $inheritedRoles->column('ID'); $inheritedRoleIDs = $inheritedRoles->column('ID');
} else { } else {
$groupRoleIDs = array(); $groupRoleIDs = array();
$inheritedRoleIDs = array(); $inheritedRoleIDs = array();
} }
$rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray()) $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
->setDefaultItems($groupRoleIDs) ->setDefaultItems($groupRoleIDs)
->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group')) ->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group'))
->setDisabledItems($inheritedRoleIDs); ->setDisabledItems($inheritedRoleIDs);
if(!$allRoles->count()) { if (!$allRoles->count()) {
$rolesField->setAttribute('data-placeholder', _t('Group.NoRoles', 'No roles found')); $rolesField->setAttribute('data-placeholder', _t('Group.NoRoles', 'No roles found'));
} }
$fields->addFieldToTab('Root.Roles', $rolesField); $fields->addFieldToTab('Root.Roles', $rolesField);
} }
$fields->push($idField = new HiddenField("ID")); $fields->push($idField = new HiddenField("ID"));
$this->extend('updateCMSFields', $fields); $this->extend('updateCMSFields', $fields);
return $fields; return $fields;
} }
/** /**
* @param bool $includerelations Indicate if the labels returned include relation fields * @param bool $includerelations Indicate if the labels returned include relation fields
* @return array * @return array
*/ */
public function fieldLabels($includerelations = true) public function fieldLabels($includerelations = true)
{ {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includerelations);
$labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name'); $labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name');
$labels['Description'] = _t('Group.Description', 'Description'); $labels['Description'] = _t('Group.Description', 'Description');
$labels['Code'] = _t('Group.Code', 'Group Code', 'Programmatical code identifying a group'); $labels['Code'] = _t('Group.Code', 'Group Code', 'Programmatical code identifying a group');
$labels['Locked'] = _t('Group.Locked', 'Locked?', 'Group is locked in the security administration area'); $labels['Locked'] = _t('Group.Locked', 'Locked?', 'Group is locked in the security administration area');
$labels['Sort'] = _t('Group.Sort', 'Sort Order'); $labels['Sort'] = _t('Group.Sort', 'Sort Order');
if($includerelations){ if ($includerelations) {
$labels['Parent'] = _t('Group.Parent', 'Parent Group', 'One group has one parent group'); $labels['Parent'] = _t('Group.Parent', 'Parent Group', 'One group has one parent group');
$labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', 'One group has many permissions'); $labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', 'One group has many permissions');
$labels['Members'] = _t('Group.many_many_Members', 'Members', 'One group has many members'); $labels['Members'] = _t('Group.many_many_Members', 'Members', 'One group has many members');
} }
return $labels; return $labels;
} }
/** /**
* Get many-many relation to {@link Member}, * Get many-many relation to {@link Member},
* including all members which are "inherited" from children groups of this record. * including all members which are "inherited" from children groups of this record.
* See {@link DirectMembers()} for retrieving members without any inheritance. * See {@link DirectMembers()} for retrieving members without any inheritance.
* *
* @param String $filter * @param String $filter
* @return ManyManyList * @return ManyManyList
*/ */
public function Members($filter = '') public function Members($filter = '')
{ {
// First get direct members as a base result // First get direct members as a base result
$result = $this->DirectMembers(); $result = $this->DirectMembers();
// Unsaved group cannot have child groups because its ID is still 0. // Unsaved group cannot have child groups because its ID is still 0.
if (!$this->exists()) { if (!$this->exists()) {
return $result; return $result;
} }
// Remove the default foreign key filter in prep for re-applying a filter containing all children groups. // Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific // Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
// ones. // ones.
if(!($result instanceof UnsavedRelationList)) { if (!($result instanceof UnsavedRelationList)) {
$result = $result->alterDataQuery(function($query){ $result = $result->alterDataQuery(function ($query) {
/** @var DataQuery $query */ /** @var DataQuery $query */
$query->removeFilterOn('Group_Members'); $query->removeFilterOn('Group_Members');
}); });
} }
// Now set all children groups as a new foreign key // Now set all children groups as a new foreign key
$groups = Group::get()->byIDs($this->collateFamilyIDs()); $groups = Group::get()->byIDs($this->collateFamilyIDs());
$result = $result->forForeignID($groups->column('ID'))->where($filter); $result = $result->forForeignID($groups->column('ID'))->where($filter);
return $result; return $result;
} }
/** /**
* Return only the members directly added to this group * Return only the members directly added to this group
*/ */
public function DirectMembers() public function DirectMembers()
{ {
return $this->getManyManyComponents('Members'); return $this->getManyManyComponents('Members');
} }
/** /**
* Return a set of this record's "family" of IDs - the IDs of * Return a set of this record's "family" of IDs - the IDs of
* this record and all its descendants. * this record and all its descendants.
* *
* @return array * @return array
*/ */
public function collateFamilyIDs() public function collateFamilyIDs()
{ {
if (!$this->exists()) { if (!$this->exists()) {
throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group."); throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group.");
} }
$familyIDs = array(); $familyIDs = array();
$chunkToAdd = array($this->ID); $chunkToAdd = array($this->ID);
while($chunkToAdd) { while ($chunkToAdd) {
$familyIDs = array_merge($familyIDs,$chunkToAdd); $familyIDs = array_merge($familyIDs, $chunkToAdd);
// Get the children of *all* the groups identified in the previous chunk. // Get the children of *all* the groups identified in the previous chunk.
// This minimises the number of SQL queries necessary // This minimises the number of SQL queries necessary
$chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID'); $chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID');
} }
return $familyIDs; return $familyIDs;
} }
/** /**
* Returns an array of the IDs of this group and all its parents * Returns an array of the IDs of this group and all its parents
* *
* @return array * @return array
*/ */
public function collateAncestorIDs() public function collateAncestorIDs()
{ {
$parent = $this; $parent = $this;
$items = []; $items = [];
while(isset($parent) && $parent instanceof Group) { while (isset($parent) && $parent instanceof Group) {
$items[] = $parent->ID; $items[] = $parent->ID;
$parent = $parent->Parent; $parent = $parent->Parent;
} }
return $items; return $items;
} }
/** /**
* This isn't a decendant of SiteTree, but needs this in case * This isn't a decendant of SiteTree, but needs this in case
* the group is "reorganised"; * the group is "reorganised";
*/ */
public function cmsCleanup_parentChanged() public function cmsCleanup_parentChanged()
{ {
} }
/** /**
* Override this so groups are ordered in the CMS * Override this so groups are ordered in the CMS
*/ */
public function stageChildren() public function stageChildren()
{ {
return Group::get() return Group::get()
->filter("ParentID", $this->ID) ->filter("ParentID", $this->ID)
->exclude("ID", $this->ID) ->exclude("ID", $this->ID)
->sort('"Sort"'); ->sort('"Sort"');
} }
public function getTreeTitle() public function getTreeTitle()
{ {
if($this->hasMethod('alternateTreeTitle')) { if ($this->hasMethod('alternateTreeTitle')) {
return $this->alternateTreeTitle(); return $this->alternateTreeTitle();
} }
return htmlspecialchars($this->Title, ENT_QUOTES); return htmlspecialchars($this->Title, ENT_QUOTES);
} }
/** /**
* Overloaded to ensure the code is always descent. * Overloaded to ensure the code is always descent.
* *
* @param string * @param string
*/ */
public function setCode($val) public function setCode($val)
{ {
$this->setField("Code", Convert::raw2url($val)); $this->setField("Code", Convert::raw2url($val));
} }
public function validate() public function validate()
{ {
$result = parent::validate(); $result = parent::validate();
// Check if the new group hierarchy would add certain "privileged permissions", // Check if the new group hierarchy would add certain "privileged permissions",
// and require an admin to perform this change in case it does. // and require an admin to perform this change in case it does.
// This prevents "sub-admin" users with group editing permissions to increase their privileges. // This prevents "sub-admin" users with group editing permissions to increase their privileges.
if($this->Parent()->exists() && !Permission::check('ADMIN')) { if ($this->Parent()->exists() && !Permission::check('ADMIN')) {
$inheritedCodes = Permission::get() $inheritedCodes = Permission::get()
->filter('GroupID', $this->Parent()->collateAncestorIDs()) ->filter('GroupID', $this->Parent()->collateAncestorIDs())
->column('Code'); ->column('Code');
$privilegedCodes = Config::inst()->get('SilverStripe\\Security\\Permission', 'privileged_permissions'); $privilegedCodes = Config::inst()->get('SilverStripe\\Security\\Permission', 'privileged_permissions');
if(array_intersect($inheritedCodes, $privilegedCodes)) { if (array_intersect($inheritedCodes, $privilegedCodes)) {
$result->addError(sprintf( $result->addError(sprintf(
_t( _t(
'Group.HierarchyPermsError', 'Group.HierarchyPermsError',
'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)' 'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
), ),
$this->Parent()->Title $this->Parent()->Title
)); ));
} }
} }
return $result; return $result;
} }
public function onBeforeWrite() public function onBeforeWrite()
{ {
parent::onBeforeWrite(); parent::onBeforeWrite();
// Only set code property when the group has a custom title, and no code exists. // Only set code property when the group has a custom title, and no code exists.
// The "Code" attribute is usually treated as a more permanent identifier than database IDs // The "Code" attribute is usually treated as a more permanent identifier than database IDs
// in custom application logic, so can't be changed after its first set. // in custom application logic, so can't be changed after its first set.
if(!$this->Code && $this->Title != _t('SecurityAdmin.NEWGROUP',"New Group")) { if (!$this->Code && $this->Title != _t('SecurityAdmin.NEWGROUP', "New Group")) {
$this->setCode($this->Title); $this->setCode($this->Title);
} }
} }
public function onBeforeDelete() public function onBeforeDelete()
{ {
parent::onBeforeDelete(); parent::onBeforeDelete();
// if deleting this group, delete it's children as well // if deleting this group, delete it's children as well
foreach($this->Groups() as $group) { foreach ($this->Groups() as $group) {
$group->delete(); $group->delete();
} }
// Delete associated permissions // Delete associated permissions
foreach($this->Permissions() as $permission) { foreach ($this->Permissions() as $permission) {
$permission->delete(); $permission->delete();
} }
} }
/** /**
* Checks for permission-code CMS_ACCESS_SecurityAdmin. * Checks for permission-code CMS_ACCESS_SecurityAdmin.
* If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well. * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
* *
* @param $member Member * @param $member Member
* @return boolean * @return boolean
*/ */
public function canEdit($member = null) public function canEdit($member = null)
{ {
if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) { if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) {
$member = Member::currentUser(); $member = Member::currentUser();
} }
// extended access checks // extended access checks
$results = $this->extend('canEdit', $member); $results = $this->extend('canEdit', $member);
if ($results && is_array($results)) { if ($results && is_array($results)) {
if (!min($results)) { if (!min($results)) {
return false; return false;
@ -490,48 +490,48 @@ class Group extends DataObject
} }
if (// either we have an ADMIN if (// either we have an ADMIN
(bool)Permission::checkMember($member, "ADMIN") (bool)Permission::checkMember($member, "ADMIN")
|| ( || (
// or a privileged CMS user and a group without ADMIN permissions. // or a privileged CMS user and a group without ADMIN permissions.
// without this check, a user would be able to add himself to an administrators group // without this check, a user would be able to add himself to an administrators group
// with just access to the "Security" admin interface // with just access to the "Security" admin interface
Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") && Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
!Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists() !Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
) )
) { ) {
return true; return true;
} }
return false; return false;
} }
/** /**
* Checks for permission-code CMS_ACCESS_SecurityAdmin. * Checks for permission-code CMS_ACCESS_SecurityAdmin.
* *
* @param $member Member * @param $member Member
* @return boolean * @return boolean
*/ */
public function canView($member = null) public function canView($member = null)
{ {
if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) { if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) {
$member = Member::currentUser(); $member = Member::currentUser();
} }
// extended access checks // extended access checks
$results = $this->extend('canView', $member); $results = $this->extend('canView', $member);
if ($results && is_array($results)) { if ($results && is_array($results)) {
if (!min($results)) { if (!min($results)) {
return false; return false;
} }
} }
// user needs access to CMS_ACCESS_SecurityAdmin // user needs access to CMS_ACCESS_SecurityAdmin
if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) { if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) {
return true; return true;
} }
return false; return false;
} }
public function canDelete($member = null) public function canDelete($member = null)
{ {
@ -539,30 +539,30 @@ class Group extends DataObject
$member = Member::currentUser(); $member = Member::currentUser();
} }
// extended access checks // extended access checks
$results = $this->extend('canDelete', $member); $results = $this->extend('canDelete', $member);
if ($results && is_array($results)) { if ($results && is_array($results)) {
if (!min($results)) { if (!min($results)) {
return false; return false;
} }
} }
return $this->canEdit($member); return $this->canEdit($member);
} }
/** /**
* Returns all of the children for the CMS Tree. * Returns all of the children for the CMS Tree.
* Filters to only those groups that the current user can edit * Filters to only those groups that the current user can edit
*/ */
public function AllChildrenIncludingDeleted() public function AllChildrenIncludingDeleted()
{ {
/** @var Hierarchy $extInstance */ /** @var Hierarchy $extInstance */
$extInstance = $this->getExtensionInstance('SilverStripe\\ORM\\Hierarchy\\Hierarchy'); $extInstance = $this->getExtensionInstance('SilverStripe\\ORM\\Hierarchy\\Hierarchy');
$extInstance->setOwner($this); $extInstance->setOwner($this);
$children = $extInstance->AllChildrenIncludingDeleted(); $children = $extInstance->AllChildrenIncludingDeleted();
$extInstance->clearOwner(); $extInstance->clearOwner();
$filteredChildren = new ArrayList(); $filteredChildren = new ArrayList();
if ($children) { if ($children) {
foreach ($children as $child) { foreach ($children as $child) {
@ -570,46 +570,46 @@ class Group extends DataObject
$filteredChildren->push($child); $filteredChildren->push($child);
} }
} }
} }
return $filteredChildren; return $filteredChildren;
} }
/** /**
* Add default records to database. * Add default records to database.
* *
* This function is called whenever the database is built, after the * This function is called whenever the database is built, after the
* database tables have all been created. * database tables have all been created.
*/ */
public function requireDefaultRecords() public function requireDefaultRecords()
{ {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
// Add default author group if no other group exists // Add default author group if no other group exists
$allGroups = DataObject::get('SilverStripe\\Security\\Group'); $allGroups = DataObject::get('SilverStripe\\Security\\Group');
if(!$allGroups->count()) { if (!$allGroups->count()) {
$authorGroup = new Group(); $authorGroup = new Group();
$authorGroup->Code = 'content-authors'; $authorGroup->Code = 'content-authors';
$authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors'); $authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors');
$authorGroup->Sort = 1; $authorGroup->Sort = 1;
$authorGroup->write(); $authorGroup->write();
Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain'); Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin'); Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin'); Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE'); Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
} }
// Add default admin group if none with permission code ADMIN exists // Add default admin group if none with permission code ADMIN exists
$adminGroups = Permission::get_groups_by_permission('ADMIN'); $adminGroups = Permission::get_groups_by_permission('ADMIN');
if(!$adminGroups->count()) { if (!$adminGroups->count()) {
$adminGroup = new Group(); $adminGroup = new Group();
$adminGroup->Code = 'administrators'; $adminGroup->Code = 'administrators';
$adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators'); $adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators');
$adminGroup->Sort = 0; $adminGroup->Sort = 0;
$adminGroup->write(); $adminGroup->write();
Permission::grant($adminGroup->ID, 'ADMIN'); Permission::grant($adminGroup->ID, 'ADMIN');
} }
// Members are populated through Member->requireDefaultRecords() // Members are populated through Member->requireDefaultRecords()
} }
} }

View File

@ -63,380 +63,380 @@ use Zend_Locale_Format;
class Member extends DataObject implements TemplateGlobalProvider class Member extends DataObject implements TemplateGlobalProvider
{ {
private static $db = array( private static $db = array(
'FirstName' => 'Varchar', 'FirstName' => 'Varchar',
'Surname' => 'Varchar', 'Surname' => 'Varchar',
'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character) 'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication 'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
'TempIDExpired' => 'Datetime', // Expiry of temp login 'TempIDExpired' => 'Datetime', // Expiry of temp login
'Password' => 'Varchar(160)', 'Password' => 'Varchar(160)',
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset 'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
'AutoLoginExpired' => 'Datetime', 'AutoLoginExpired' => 'Datetime',
// This is an arbitrary code pointing to a PasswordEncryptor instance, // This is an arbitrary code pointing to a PasswordEncryptor instance,
// not an actual encryption algorithm. // not an actual encryption algorithm.
// Warning: Never change this field after its the first password hashing without // Warning: Never change this field after its the first password hashing without
// providing a new cleartext password as well. // providing a new cleartext password as well.
'PasswordEncryption' => "Varchar(50)", 'PasswordEncryption' => "Varchar(50)",
'Salt' => 'Varchar(50)', 'Salt' => 'Varchar(50)',
'PasswordExpiry' => 'Date', 'PasswordExpiry' => 'Date',
'LockedOutUntil' => 'Datetime', 'LockedOutUntil' => 'Datetime',
'Locale' => 'Varchar(6)', 'Locale' => 'Varchar(6)',
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
'FailedLoginCount' => 'Int', 'FailedLoginCount' => 'Int',
// In ISO format // In ISO format
'DateFormat' => 'Varchar(30)', 'DateFormat' => 'Varchar(30)',
'TimeFormat' => 'Varchar(30)', 'TimeFormat' => 'Varchar(30)',
); );
private static $belongs_many_many = array( private static $belongs_many_many = array(
'Groups' => 'SilverStripe\\Security\\Group', 'Groups' => 'SilverStripe\\Security\\Group',
); );
private static $has_many = array( private static $has_many = array(
'LoggedPasswords' => 'SilverStripe\\Security\\MemberPassword', 'LoggedPasswords' => 'SilverStripe\\Security\\MemberPassword',
'RememberLoginHashes' => 'SilverStripe\\Security\\RememberLoginHash' 'RememberLoginHashes' => 'SilverStripe\\Security\\RememberLoginHash'
); );
private static $table_name = "Member"; private static $table_name = "Member";
private static $default_sort = '"Surname", "FirstName"'; private static $default_sort = '"Surname", "FirstName"';
private static $indexes = array( private static $indexes = array(
'Email' => true, 'Email' => true,
//Removed due to duplicate null values causing MSSQL problems //Removed due to duplicate null values causing MSSQL problems
//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true) //'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
); );
/** /**
* @config * @config
* @var boolean * @var boolean
*/ */
private static $notify_password_change = false; private static $notify_password_change = false;
/** /**
* All searchable database columns * All searchable database columns
* in this object, currently queried * in this object, currently queried
* with a "column LIKE '%keywords%' * with a "column LIKE '%keywords%'
* statement. * statement.
* *
* @var array * @var array
* @todo Generic implementation of $searchable_fields on DataObject, * @todo Generic implementation of $searchable_fields on DataObject,
* with definition for different searching algorithms * with definition for different searching algorithms
* (LIKE, FULLTEXT) and default FormFields to construct a searchform. * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
*/ */
private static $searchable_fields = array( private static $searchable_fields = array(
'FirstName', 'FirstName',
'Surname', 'Surname',
'Email', 'Email',
); );
/** /**
* @config * @config
* @var array * @var array
*/ */
private static $summary_fields = array( private static $summary_fields = array(
'FirstName', 'FirstName',
'Surname', 'Surname',
'Email', 'Email',
); );
/** /**
* @config * @config
* @var array * @var array
*/ */
private static $casting = array( private static $casting = array(
'Name' => 'Varchar', 'Name' => 'Varchar',
); );
/** /**
* Internal-use only fields * Internal-use only fields
* *
* @config * @config
* @var array * @var array
*/ */
private static $hidden_fields = array( private static $hidden_fields = array(
'AutoLoginHash', 'AutoLoginHash',
'AutoLoginExpired', 'AutoLoginExpired',
'PasswordEncryption', 'PasswordEncryption',
'PasswordExpiry', 'PasswordExpiry',
'LockedOutUntil', 'LockedOutUntil',
'TempIDHash', 'TempIDHash',
'TempIDExpired', 'TempIDExpired',
'Salt', 'Salt',
); );
/** /**
* @config * @config
* @var array See {@link set_title_columns()} * @var array See {@link set_title_columns()}
*/ */
private static $title_format = null; private static $title_format = null;
/** /**
* The unique field used to identify this member. * The unique field used to identify this member.
* By default, it's "Email", but another common * By default, it's "Email", but another common
* field could be Username. * field could be Username.
* *
* @config * @config
* @var string * @var string
* @skipUpgrade * @skipUpgrade
*/ */
private static $unique_identifier_field = 'Email'; private static $unique_identifier_field = 'Email';
/** /**
* Object for validating user's password * Object for validating user's password
* *
* @config * @config
* @var PasswordValidator * @var PasswordValidator
*/ */
private static $password_validator = null; private static $password_validator = null;
/** /**
* @config * @config
* The number of days that a password should be valid for. * The number of days that a password should be valid for.
* By default, this is null, which means that passwords never expire * By default, this is null, which means that passwords never expire
*/ */
private static $password_expiry_days = null; private static $password_expiry_days = null;
/** /**
* @config * @config
* @var Int Number of incorrect logins after which * @var Int Number of incorrect logins after which
* the user is blocked from further attempts for the timespan * the user is blocked from further attempts for the timespan
* defined in {@link $lock_out_delay_mins}. * defined in {@link $lock_out_delay_mins}.
*/ */
private static $lock_out_after_incorrect_logins = 10; private static $lock_out_after_incorrect_logins = 10;
/** /**
* @config * @config
* @var integer Minutes of enforced lockout after incorrect password attempts. * @var integer Minutes of enforced lockout after incorrect password attempts.
* Only applies if {@link $lock_out_after_incorrect_logins} greater than 0. * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
*/ */
private static $lock_out_delay_mins = 15; private static $lock_out_delay_mins = 15;
/** /**
* @config * @config
* @var String If this is set, then a session cookie with the given name will be set on log-in, * @var String If this is set, then a session cookie with the given name will be set on log-in,
* and cleared on logout. * and cleared on logout.
*/ */
private static $login_marker_cookie = null; private static $login_marker_cookie = null;
/** /**
* Indicates that when a {@link Member} logs in, Member:session_regenerate_id() * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
* should be called as a security precaution. * should be called as a security precaution.
* *
* This doesn't always work, especially if you're trying to set session cookies * This doesn't always work, especially if you're trying to set session cookies
* across an entire site using the domain parameter to session_set_cookie_params() * across an entire site using the domain parameter to session_set_cookie_params()
* *
* @config * @config
* @var boolean * @var boolean
*/ */
private static $session_regenerate_id = true; private static $session_regenerate_id = true;
/** /**
* Default lifetime of temporary ids. * Default lifetime of temporary ids.
* *
* This is the period within which a user can be re-authenticated within the CMS by entering only their password * This is the period within which a user can be re-authenticated within the CMS by entering only their password
* and without losing their workspace. * and without losing their workspace.
* *
* Any session expiration outside of this time will require them to login from the frontend using their full * Any session expiration outside of this time will require them to login from the frontend using their full
* username and password. * username and password.
* *
* Defaults to 72 hours. Set to zero to disable expiration. * Defaults to 72 hours. Set to zero to disable expiration.
* *
* @config * @config
* @var int Lifetime in seconds * @var int Lifetime in seconds
*/ */
private static $temp_id_lifetime = 259200; private static $temp_id_lifetime = 259200;
/** /**
* Ensure the locale is set to something sensible by default. * Ensure the locale is set to something sensible by default.
*/ */
public function populateDefaults() public function populateDefaults()
{ {
parent::populateDefaults(); parent::populateDefaults();
$this->Locale = i18n::get_closest_translation(i18n::get_locale()); $this->Locale = i18n::get_closest_translation(i18n::get_locale());
} }
public function requireDefaultRecords() public function requireDefaultRecords()
{ {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
// Default groups should've been built by Group->requireDefaultRecords() already // Default groups should've been built by Group->requireDefaultRecords() already
static::default_admin(); static::default_admin();
} }
/** /**
* Get the default admin record if it exists, or creates it otherwise if enabled * Get the default admin record if it exists, or creates it otherwise if enabled
* *
* @return Member * @return Member
*/ */
public static function default_admin() public static function default_admin()
{ {
// Check if set // Check if set
if (!Security::has_default_admin()) { if (!Security::has_default_admin()) {
return null; return null;
} }
// Find or create ADMIN group // Find or create ADMIN group
Group::singleton()->requireDefaultRecords(); Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first(); $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
// Find member // Find member
/** @skipUpgrade */ /** @skipUpgrade */
$admin = Member::get() $admin = Member::get()
->filter('Email', Security::default_admin_username()) ->filter('Email', Security::default_admin_username())
->first(); ->first();
if(!$admin) { if (!$admin) {
// 'Password' is not set to avoid creating // 'Password' is not set to avoid creating
// persistent logins in the database. See Security::setDefaultAdmin(). // persistent logins in the database. See Security::setDefaultAdmin().
// Set 'Email' to identify this as the default admin // Set 'Email' to identify this as the default admin
$admin = Member::create(); $admin = Member::create();
$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin'); $admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
$admin->Email = Security::default_admin_username(); $admin->Email = Security::default_admin_username();
$admin->write(); $admin->write();
} }
// Ensure this user is in the admin group // Ensure this user is in the admin group
if(!$admin->inGroup($adminGroup)) { if (!$admin->inGroup($adminGroup)) {
// Add member to group instead of adding group to member // Add member to group instead of adding group to member
// This bypasses the privilege escallation code in Member_GroupSet // This bypasses the privilege escallation code in Member_GroupSet
$adminGroup $adminGroup
->DirectMembers() ->DirectMembers()
->add($admin); ->add($admin);
} }
return $admin; return $admin;
} }
/** /**
* Check if the passed password matches the stored one (if the member is not locked out). * Check if the passed password matches the stored one (if the member is not locked out).
* *
* @param string $password * @param string $password
* @return ValidationResult * @return ValidationResult
*/ */
public function checkPassword($password) public function checkPassword($password)
{ {
$result = $this->canLogIn(); $result = $this->canLogIn();
// Short-circuit the result upon failure, no further checks needed. // Short-circuit the result upon failure, no further checks needed.
if (!$result->valid()) { if (!$result->isValid()) {
return $result; return $result;
} }
// Allow default admin to login as self // Allow default admin to login as self
if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) { if ($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
return $result; return $result;
} }
// Check a password is set on this member // Check a password is set on this member
if(empty($this->Password) && $this->exists()) { if (empty($this->Password) && $this->exists()) {
$result->addError(_t('Member.NoPassword','There is no password on this member.')); $result->addError(_t('Member.NoPassword', 'There is no password on this member.'));
return $result; return $result;
} }
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
if(!$e->check($this->Password, $password, $this->Salt, $this)) { if (!$e->check($this->Password, $password, $this->Salt, $this)) {
$result->addError(_t ( $result->addError(_t(
'Member.ERRORWRONGCRED', 'Member.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.' 'The provided details don\'t seem to be correct. Please try again.'
)); ));
} }
return $result; return $result;
} }
/** /**
* Check if this user is the currently configured default admin * Check if this user is the currently configured default admin
* *
* @return bool * @return bool
*/ */
public function isDefaultAdmin() public function isDefaultAdmin()
{ {
return Security::has_default_admin() return Security::has_default_admin()
&& $this->Email === Security::default_admin_username(); && $this->Email === Security::default_admin_username();
} }
/** /**
* Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
* one with error messages to display if the member is locked out. * one with error messages to display if the member is locked out.
* *
* You can hook into this with a "canLogIn" method on an attached extension. * You can hook into this with a "canLogIn" method on an attached extension.
* *
* @return ValidationResult * @return ValidationResult
*/ */
public function canLogIn() public function canLogIn()
{ {
$result = ValidationResult::create(); $result = ValidationResult::create();
if($this->isLockedOut()) { if ($this->isLockedOut()) {
$result->addError( $result->addError(
_t( _t(
'Member.ERRORLOCKEDOUT2', 'Member.ERRORLOCKEDOUT2',
'Your account has been temporarily disabled because of too many failed attempts at ' . 'Your account has been temporarily disabled because of too many failed attempts at ' .
'logging in. Please try again in {count} minutes.', 'logging in. Please try again in {count} minutes.',
null, null,
array('count' => $this->config()->lock_out_delay_mins) array('count' => $this->config()->lock_out_delay_mins)
) )
); );
} }
$this->extend('canLogIn', $result); $this->extend('canLogIn', $result);
return $result; return $result;
} }
/** /**
* Returns true if this user is locked out * Returns true if this user is locked out
*/ */
public function isLockedOut() public function isLockedOut()
{ {
return $this->LockedOutUntil && DBDatetime::now()->Format('U') < strtotime($this->LockedOutUntil); return $this->LockedOutUntil && DBDatetime::now()->Format('U') < strtotime($this->LockedOutUntil);
} }
/** /**
* Regenerate the session_id. * Regenerate the session_id.
* This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to. * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
* They have caused problems in certain * They have caused problems in certain
* quirky problems (such as using the Windmill 0.3.6 proxy). * quirky problems (such as using the Windmill 0.3.6 proxy).
*/ */
public static function session_regenerate_id() public static function session_regenerate_id()
{ {
if (!self::config()->session_regenerate_id) { if (!self::config()->session_regenerate_id) {
return; return;
} }
// This can be called via CLI during testing. // This can be called via CLI during testing.
if (Director::is_cli()) { if (Director::is_cli()) {
return; return;
} }
$file = ''; $file = '';
$line = ''; $line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly // @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function! // There's nothing we can do about this, because it's an operating system function!
if (!headers_sent($file, $line)) { if (!headers_sent($file, $line)) {
@session_regenerate_id(true); @session_regenerate_id(true);
} }
} }
/** /**
* Set a {@link PasswordValidator} object to use to validate member's passwords. * Set a {@link PasswordValidator} object to use to validate member's passwords.
* *
* @param PasswordValidator $pv * @param PasswordValidator $pv
*/ */
public static function set_password_validator($pv) public static function set_password_validator($pv)
{ {
self::$password_validator = $pv; self::$password_validator = $pv;
} }
/** /**
* Returns the current {@link PasswordValidator} * Returns the current {@link PasswordValidator}
* *
* @return PasswordValidator * @return PasswordValidator
*/ */
public static function password_validator() public static function password_validator()
{ {
return self::$password_validator; return self::$password_validator;
} }
public function isPasswordExpired() public function isPasswordExpired()
@ -444,176 +444,176 @@ class Member extends DataObject implements TemplateGlobalProvider
if (!$this->PasswordExpiry) { if (!$this->PasswordExpiry) {
return false; return false;
} }
return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry); return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
} }
/** /**
* Logs this member in * Logs this member in
* *
* @param bool $remember If set to TRUE, the member will be logged in automatically the next time. * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
*/ */
public function logIn($remember = false) public function logIn($remember = false)
{ {
$this->extend('beforeMemberLoggedIn'); $this->extend('beforeMemberLoggedIn');
self::session_regenerate_id(); self::session_regenerate_id();
Session::set("loggedInAs", $this->ID); Session::set("loggedInAs", $this->ID);
// This lets apache rules detect whether the user has logged in // This lets apache rules detect whether the user has logged in
if (Member::config()->login_marker_cookie) { if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0); Cookie::set(Member::config()->login_marker_cookie, 1, 0);
} }
if (Security::config()->autologin_enabled) { if (Security::config()->autologin_enabled) {
// Cleans up any potential previous hash for this member on this device // Cleans up any potential previous hash for this member on this device
if ($alcDevice = Cookie::get('alc_device')) { if ($alcDevice = Cookie::get('alc_device')) {
RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll(); RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
} }
if($remember) { if ($remember) {
$rememberLoginHash = RememberLoginHash::generate($this); $rememberLoginHash = RememberLoginHash::generate($this);
$tokenExpiryDays = Config::inst()->get( $tokenExpiryDays = Config::inst()->get(
'SilverStripe\\Security\\RememberLoginHash', 'SilverStripe\\Security\\RememberLoginHash',
'token_expiry_days' 'token_expiry_days'
); );
$deviceExpiryDays = Config::inst()->get( $deviceExpiryDays = Config::inst()->get(
'SilverStripe\\Security\\RememberLoginHash', 'SilverStripe\\Security\\RememberLoginHash',
'device_expiry_days' 'device_expiry_days'
); );
Cookie::set( Cookie::set(
'alc_enc', 'alc_enc',
$this->ID . ':' . $rememberLoginHash->getToken(), $this->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays, $tokenExpiryDays,
null, null,
null, null,
null, null,
true true
); );
Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true); Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
} else { } else {
Cookie::set('alc_enc', null); Cookie::set('alc_enc', null);
Cookie::set('alc_device', null); Cookie::set('alc_device', null);
Cookie::force_expiry('alc_enc'); Cookie::force_expiry('alc_enc');
Cookie::force_expiry('alc_device'); Cookie::force_expiry('alc_device');
} }
} }
// Clear the incorrect log-in count // Clear the incorrect log-in count
$this->registerSuccessfulLogin(); $this->registerSuccessfulLogin();
$this->LockedOutUntil = null; $this->LockedOutUntil = null;
$this->regenerateTempID(); $this->regenerateTempID();
$this->write(); $this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedIn'); $this->extend('memberLoggedIn');
} }
/** /**
* Trigger regeneration of TempID. * Trigger regeneration of TempID.
* *
* This should be performed any time the user presents their normal identification (normally Email) * This should be performed any time the user presents their normal identification (normally Email)
* and is successfully authenticated. * and is successfully authenticated.
*/ */
public function regenerateTempID() public function regenerateTempID()
{ {
$generator = new RandomGenerator(); $generator = new RandomGenerator();
$this->TempIDHash = $generator->randomToken('sha1'); $this->TempIDHash = $generator->randomToken('sha1');
$this->TempIDExpired = self::config()->temp_id_lifetime $this->TempIDExpired = self::config()->temp_id_lifetime
? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime) ? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
: null; : null;
$this->write(); $this->write();
} }
/** /**
* Check if the member ID logged in session actually * Check if the member ID logged in session actually
* has a database record of the same ID. If there is * has a database record of the same ID. If there is
* no logged in user, FALSE is returned anyway. * no logged in user, FALSE is returned anyway.
* *
* @return boolean TRUE record found FALSE no record found * @return boolean TRUE record found FALSE no record found
*/ */
public static function logged_in_session_exists() public static function logged_in_session_exists()
{ {
if($id = Member::currentUserID()) { if ($id = Member::currentUserID()) {
if($member = DataObject::get_by_id('SilverStripe\\Security\\Member', $id)) { if ($member = DataObject::get_by_id('SilverStripe\\Security\\Member', $id)) {
if ($member->exists()) { if ($member->exists()) {
return true; return true;
} }
} }
} }
return false; return false;
} }
/** /**
* Log the user in if the "remember login" cookie is set * Log the user in if the "remember login" cookie is set
* *
* The <i>remember login token</i> will be changed on every successful * The <i>remember login token</i> will be changed on every successful
* auto-login. * auto-login.
*/ */
public static function autoLogin() public static function autoLogin()
{ {
// Don't bother trying this multiple times // Don't bother trying this multiple times
if (!class_exists('SilverStripe\\Dev\\SapphireTest', false) || !SapphireTest::is_running_test()) { if (!class_exists('SilverStripe\\Dev\\SapphireTest', false) || !SapphireTest::is_running_test()) {
self::$_already_tried_to_auto_log_in = true; self::$_already_tried_to_auto_log_in = true;
} }
if(!Security::config()->autologin_enabled if (!Security::config()->autologin_enabled
|| strpos(Cookie::get('alc_enc'), ':') === false || strpos(Cookie::get('alc_enc'), ':') === false
|| Session::get("loggedInAs") || Session::get("loggedInAs")
|| !Security::database_is_ready() || !Security::database_is_ready()
) { ) {
return; return;
} }
if(strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) { if (strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2); list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
if (!$uid || !$token) { if (!$uid || !$token) {
return; return;
} }
$deviceID = Cookie::get('alc_device'); $deviceID = Cookie::get('alc_device');
/** @var Member $member */ /** @var Member $member */
$member = Member::get()->byID($uid); $member = Member::get()->byID($uid);
/** @var RememberLoginHash $rememberLoginHash */ /** @var RememberLoginHash $rememberLoginHash */
$rememberLoginHash = null; $rememberLoginHash = null;
// check if autologin token matches // check if autologin token matches
if($member) { if ($member) {
$hash = $member->encryptWithUserSettings($token); $hash = $member->encryptWithUserSettings($token);
$rememberLoginHash = RememberLoginHash::get() $rememberLoginHash = RememberLoginHash::get()
->filter(array( ->filter(array(
'MemberID' => $member->ID, 'MemberID' => $member->ID,
'DeviceID' => $deviceID, 'DeviceID' => $deviceID,
'Hash' => $hash 'Hash' => $hash
))->first(); ))->first();
if(!$rememberLoginHash) { if (!$rememberLoginHash) {
$member = null; $member = null;
} else { } else {
// Check for expired token // Check for expired token
$expiryDate = new DateTime($rememberLoginHash->ExpiryDate); $expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
$now = DBDatetime::now(); $now = DBDatetime::now();
$now = new DateTime($now->Rfc2822()); $now = new DateTime($now->Rfc2822());
if ($now > $expiryDate) { if ($now > $expiryDate) {
$member = null; $member = null;
} }
} }
} }
if($member) { if ($member) {
self::session_regenerate_id(); self::session_regenerate_id();
Session::set("loggedInAs", $member->ID); Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in // This lets apache rules detect whether the user has logged in
if(Member::config()->login_marker_cookie) { if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true); Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
} }
if ($rememberLoginHash) { if ($rememberLoginHash) {
$rememberLoginHash->renew(); $rememberLoginHash->renew();
$tokenExpiryDays = RememberLoginHash::config()->get('token_expiry_days'); $tokenExpiryDays = RememberLoginHash::config()->get('token_expiry_days');
Cookie::set( Cookie::set(
'alc_enc', 'alc_enc',
$member->ID . ':' . $rememberLoginHash->getToken(), $member->ID . ':' . $rememberLoginHash->getToken(),
@ -623,272 +623,272 @@ class Member extends DataObject implements TemplateGlobalProvider
false, false,
true true
); );
} }
$member->write(); $member->write();
// Audit logging hook // Audit logging hook
$member->extend('memberAutoLoggedIn'); $member->extend('memberAutoLoggedIn');
} }
} }
} }
/** /**
* Logs this member out. * Logs this member out.
*/ */
public function logOut() public function logOut()
{ {
$this->extend('beforeMemberLoggedOut'); $this->extend('beforeMemberLoggedOut');
Session::clear("loggedInAs"); Session::clear("loggedInAs");
if (Member::config()->login_marker_cookie) { if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, null, 0); Cookie::set(Member::config()->login_marker_cookie, null, 0);
} }
Session::destroy(); Session::destroy();
$this->extend('memberLoggedOut'); $this->extend('memberLoggedOut');
// Clears any potential previous hashes for this member // Clears any potential previous hashes for this member
RememberLoginHash::clear($this, Cookie::get('alc_device')); RememberLoginHash::clear($this, Cookie::get('alc_device'));
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
Cookie::force_expiry('alc_enc'); Cookie::force_expiry('alc_enc');
Cookie::set('alc_device', null); Cookie::set('alc_device', null);
Cookie::force_expiry('alc_device'); Cookie::force_expiry('alc_device');
// Switch back to live in order to avoid infinite loops when // Switch back to live in order to avoid infinite loops when
// redirecting to the login screen (if this login screen is versioned) // redirecting to the login screen (if this login screen is versioned)
Session::clear('readingMode'); Session::clear('readingMode');
$this->write(); $this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedOut'); $this->extend('memberLoggedOut');
} }
/** /**
* Utility for generating secure password hashes for this member. * Utility for generating secure password hashes for this member.
* *
* @param string $string * @param string $string
* @return string * @return string
* @throws PasswordEncryptor_NotFoundException * @throws PasswordEncryptor_NotFoundException
*/ */
public function encryptWithUserSettings($string) public function encryptWithUserSettings($string)
{ {
if (!$string) { if (!$string) {
return null; return null;
} }
// If the algorithm or salt is not available, it means we are operating // If the algorithm or salt is not available, it means we are operating
// on legacy account with unhashed password. Do not hash the string. // on legacy account with unhashed password. Do not hash the string.
if (!$this->PasswordEncryption) { if (!$this->PasswordEncryption) {
return $string; return $string;
} }
// We assume we have PasswordEncryption and Salt available here. // We assume we have PasswordEncryption and Salt available here.
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
return $e->encrypt($string, $this->Salt); return $e->encrypt($string, $this->Salt);
} }
/** /**
* Generate an auto login token which can be used to reset the password, * Generate an auto login token which can be used to reset the password,
* at the same time hashing it and storing in the database. * at the same time hashing it and storing in the database.
* *
* @param int $lifetime The lifetime of the auto login hash in days (by default 2 days) * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
* *
* @returns string Token that should be passed to the client (but NOT persisted). * @returns string Token that should be passed to the client (but NOT persisted).
* *
* @todo Make it possible to handle database errors such as a "duplicate key" error * @todo Make it possible to handle database errors such as a "duplicate key" error
*/ */
public function generateAutologinTokenAndStoreHash($lifetime = 2) public function generateAutologinTokenAndStoreHash($lifetime = 2)
{ {
do { do {
$generator = new RandomGenerator(); $generator = new RandomGenerator();
$token = $generator->randomToken(); $token = $generator->randomToken();
$hash = $this->encryptWithUserSettings($token); $hash = $this->encryptWithUserSettings($token);
} while(DataObject::get_one('SilverStripe\\Security\\Member', array( } while (DataObject::get_one('SilverStripe\\Security\\Member', array(
'"Member"."AutoLoginHash"' => $hash '"Member"."AutoLoginHash"' => $hash
))); )));
$this->AutoLoginHash = $hash; $this->AutoLoginHash = $hash;
$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime)); $this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
$this->write(); $this->write();
return $token; return $token;
} }
/** /**
* Check the token against the member. * Check the token against the member.
* *
* @param string $autologinToken * @param string $autologinToken
* *
* @returns bool Is token valid? * @returns bool Is token valid?
*/ */
public function validateAutoLoginToken($autologinToken) public function validateAutoLoginToken($autologinToken)
{ {
$hash = $this->encryptWithUserSettings($autologinToken); $hash = $this->encryptWithUserSettings($autologinToken);
$member = self::member_from_autologinhash($hash, false); $member = self::member_from_autologinhash($hash, false);
return (bool)$member; return (bool)$member;
} }
/** /**
* Return the member for the auto login hash * Return the member for the auto login hash
* *
* @param string $hash The hash key * @param string $hash The hash key
* @param bool $login Should the member be logged in? * @param bool $login Should the member be logged in?
* *
* @return Member the matching member, if valid * @return Member the matching member, if valid
* @return Member * @return Member
*/ */
public static function member_from_autologinhash($hash, $login = false) public static function member_from_autologinhash($hash, $login = false)
{ {
$nowExpression = DB::get_conn()->now(); $nowExpression = DB::get_conn()->now();
/** @var Member $member */ /** @var Member $member */
$member = DataObject::get_one('SilverStripe\\Security\\Member', array( $member = DataObject::get_one('SilverStripe\\Security\\Member', array(
"\"Member\".\"AutoLoginHash\"" => $hash, "\"Member\".\"AutoLoginHash\"" => $hash,
"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised "\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
)); ));
if ($login && $member) { if ($login && $member) {
$member->logIn(); $member->logIn();
} }
return $member; return $member;
} }
/** /**
* Find a member record with the given TempIDHash value * Find a member record with the given TempIDHash value
* *
* @param string $tempid * @param string $tempid
* @return Member * @return Member
*/ */
public static function member_from_tempid($tempid) public static function member_from_tempid($tempid)
{ {
$members = Member::get() $members = Member::get()
->filter('TempIDHash', $tempid); ->filter('TempIDHash', $tempid);
// Exclude expired // Exclude expired
if(static::config()->temp_id_lifetime) { if (static::config()->temp_id_lifetime) {
$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue()); $members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
} }
return $members->first(); return $members->first();
} }
/** /**
* Returns the fields for the member form - used in the registration/profile module. * Returns the fields for the member form - used in the registration/profile module.
* It should return fields that are editable by the admin and the logged-in user. * It should return fields that are editable by the admin and the logged-in user.
* *
* @return FieldList Returns a {@link FieldList} containing the fields for * @return FieldList Returns a {@link FieldList} containing the fields for
* the member form. * the member form.
*/ */
public function getMemberFormFields() public function getMemberFormFields()
{ {
$fields = parent::getFrontEndFields(); $fields = parent::getFrontEndFields();
$fields->replaceField('Password', $this->getMemberPasswordField()); $fields->replaceField('Password', $this->getMemberPasswordField());
$fields->replaceField('Locale', new DropdownField ( $fields->replaceField('Locale', new DropdownField(
'Locale', 'Locale',
$this->fieldLabel('Locale'), $this->fieldLabel('Locale'),
i18n::get_existing_translations() i18n::get_existing_translations()
)); ));
$fields->removeByName(static::config()->hidden_fields); $fields->removeByName(static::config()->hidden_fields);
$fields->removeByName('FailedLoginCount'); $fields->removeByName('FailedLoginCount');
$this->extend('updateMemberFormFields', $fields); $this->extend('updateMemberFormFields', $fields);
return $fields; return $fields;
} }
/** /**
* Builds "Change / Create Password" field for this member * Builds "Change / Create Password" field for this member
* *
* @return ConfirmedPasswordField * @return ConfirmedPasswordField
*/ */
public function getMemberPasswordField() public function getMemberPasswordField()
{ {
$editingPassword = $this->isInDB(); $editingPassword = $this->isInDB();
$label = $editingPassword $label = $editingPassword
? _t('Member.EDIT_PASSWORD', 'New Password') ? _t('Member.EDIT_PASSWORD', 'New Password')
: $this->fieldLabel('Password'); : $this->fieldLabel('Password');
/** @var ConfirmedPasswordField $password */ /** @var ConfirmedPasswordField $password */
$password = ConfirmedPasswordField::create( $password = ConfirmedPasswordField::create(
'Password', 'Password',
$label, $label,
null, null,
null, null,
$editingPassword $editingPassword
); );
// If editing own password, require confirmation of existing // If editing own password, require confirmation of existing
if($editingPassword && $this->ID == Member::currentUserID()) { if ($editingPassword && $this->ID == Member::currentUserID()) {
$password->setRequireExistingPassword(true); $password->setRequireExistingPassword(true);
} }
$password->setCanBeEmpty(true); $password->setCanBeEmpty(true);
$this->extend('updateMemberPasswordField', $password); $this->extend('updateMemberPasswordField', $password);
return $password; return $password;
} }
/** /**
* Returns the {@link RequiredFields} instance for the Member object. This * Returns the {@link RequiredFields} instance for the Member object. This
* Validator is used when saving a {@link CMSProfileController} or added to * Validator is used when saving a {@link CMSProfileController} or added to
* any form responsible for saving a users data. * any form responsible for saving a users data.
* *
* To customize the required fields, add a {@link DataExtension} to member * To customize the required fields, add a {@link DataExtension} to member
* calling the `updateValidator()` method. * calling the `updateValidator()` method.
* *
* @return Member_Validator * @return Member_Validator
*/ */
public function getValidator() public function getValidator()
{ {
$validator = Injector::inst()->create('SilverStripe\\Security\\Member_Validator'); $validator = Injector::inst()->create('SilverStripe\\Security\\Member_Validator');
$validator->setForMember($this); $validator->setForMember($this);
$this->extend('updateValidator', $validator); $this->extend('updateValidator', $validator);
return $validator; return $validator;
} }
/** /**
* Returns the current logged in user * Returns the current logged in user
* *
* @return Member * @return Member
*/ */
public static function currentUser() public static function currentUser()
{ {
$id = Member::currentUserID(); $id = Member::currentUserID();
if($id) { if ($id) {
return DataObject::get_by_id('SilverStripe\\Security\\Member', $id); return DataObject::get_by_id('SilverStripe\\Security\\Member', $id);
} }
} }
/** /**
* Get the ID of the current logged in user * Get the ID of the current logged in user
* *
* @return int Returns the ID of the current logged in user or 0. * @return int Returns the ID of the current logged in user or 0.
*/ */
public static function currentUserID() public static function currentUserID()
{ {
$id = Session::get("loggedInAs"); $id = Session::get("loggedInAs");
if(!$id && !self::$_already_tried_to_auto_log_in) { if (!$id && !self::$_already_tried_to_auto_log_in) {
self::autoLogin(); self::autoLogin();
$id = Session::get("loggedInAs"); $id = Session::get("loggedInAs");
} }
return is_numeric($id) ? $id : 0; return is_numeric($id) ? $id : 0;
} }
private static $_already_tried_to_auto_log_in = false; private static $_already_tried_to_auto_log_in = false;
/* /*
* Generate a random password, with randomiser to kick in if there's no words file on the * Generate a random password, with randomiser to kick in if there's no words file on the
* filesystem. * filesystem.
* *
@ -896,178 +896,178 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public static function create_new_password() public static function create_new_password()
{ {
$words = Config::inst()->get('SilverStripe\\Security\\Security', 'word_list'); $words = Config::inst()->get('SilverStripe\\Security\\Security', 'word_list');
if($words && file_exists($words)) { if ($words && file_exists($words)) {
$words = file($words); $words = file($words);
list($usec, $sec) = explode(' ', microtime()); list($usec, $sec) = explode(' ', microtime());
srand($sec + ((float) $usec * 100000)); srand($sec + ((float) $usec * 100000));
$word = trim($words[rand(0,sizeof($words)-1)]); $word = trim($words[rand(0, sizeof($words)-1)]);
$number = rand(10,999); $number = rand(10, 999);
return $word . $number; return $word . $number;
} else { } else {
$random = rand(); $random = rand();
$string = md5($random); $string = md5($random);
$output = substr($string, 0, 8); $output = substr($string, 0, 8);
return $output; return $output;
} }
} }
/** /**
* Event handler called before writing to the database. * Event handler called before writing to the database.
*/ */
public function onBeforeWrite() public function onBeforeWrite()
{ {
if ($this->SetPassword) { if ($this->SetPassword) {
$this->Password = $this->SetPassword; $this->Password = $this->SetPassword;
} }
// If a member with the same "unique identifier" already exists with a different ID, don't allow merging. // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
// but rather a last line of defense against data inconsistencies. // but rather a last line of defense against data inconsistencies.
$identifierField = Member::config()->unique_identifier_field; $identifierField = Member::config()->unique_identifier_field;
if($this->$identifierField) { if ($this->$identifierField) {
// Note: Same logic as Member_Validator class // Note: Same logic as Member_Validator class
$filter = array("\"$identifierField\"" => $this->$identifierField); $filter = array("\"$identifierField\"" => $this->$identifierField);
if($this->ID) { if ($this->ID) {
$filter[] = array('"Member"."ID" <> ?' => $this->ID); $filter[] = array('"Member"."ID" <> ?' => $this->ID);
} }
$existingRecord = DataObject::get_one('SilverStripe\\Security\\Member', $filter); $existingRecord = DataObject::get_one('SilverStripe\\Security\\Member', $filter);
if($existingRecord) { if ($existingRecord) {
throw new ValidationException(ValidationResult::create()->adderror(_t( throw new ValidationException(_t(
'Member.ValidationIdentifierFailed', 'Member.ValidationIdentifierFailed',
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
'Values in brackets show "fieldname = value", usually denoting an existing email address', 'Values in brackets show "fieldname = value", usually denoting an existing email address',
array( array(
'id' => $existingRecord->ID, 'id' => $existingRecord->ID,
'name' => $identifierField, 'name' => $identifierField,
'value' => $this->$identifierField 'value' => $this->$identifierField
) )
))); ));
} }
} }
// We don't send emails out on dev/tests sites to prevent accidentally spamming users. // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
// However, if TestMailer is in use this isn't a risk. // However, if TestMailer is in use this isn't a risk.
if ((Director::isLive() || Email::mailer() instanceof TestMailer) if ((Director::isLive() || Email::mailer() instanceof TestMailer)
&& $this->isChanged('Password') && $this->isChanged('Password')
&& $this->record['Password'] && $this->record['Password']
&& $this->config()->notify_password_change && $this->config()->notify_password_change
) { ) {
/** @var Email $e */ /** @var Email $e */
$e = Email::create(); $e = Email::create();
$e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')); $e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'));
$e->setTemplate('ChangePasswordEmail'); $e->setTemplate('ChangePasswordEmail');
$e->populateTemplate($this); $e->populateTemplate($this);
$e->setTo($this->Email); $e->setTo($this->Email);
$e->send(); $e->send();
} }
// The test on $this->ID is used for when records are initially created. // The test on $this->ID is used for when records are initially created.
// Note that this only works with cleartext passwords, as we can't rehash // Note that this only works with cleartext passwords, as we can't rehash
// existing passwords. // existing passwords.
if((!$this->ID && $this->Password) || $this->isChanged('Password')) { if ((!$this->ID && $this->Password) || $this->isChanged('Password')) {
//reset salt so that it gets regenerated - this will invalidate any persistant login cookies //reset salt so that it gets regenerated - this will invalidate any persistant login cookies
// or other information encrypted with this Member's settings (see self::encryptWithUserSettings) // or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
$this->Salt = ''; $this->Salt = '';
// Password was changed: encrypt the password according the settings // Password was changed: encrypt the password according the settings
$encryption_details = Security::encrypt_password( $encryption_details = Security::encrypt_password(
$this->Password, // this is assumed to be cleartext $this->Password, // this is assumed to be cleartext
$this->Salt, $this->Salt,
($this->PasswordEncryption) ? ($this->PasswordEncryption) ?
$this->PasswordEncryption : Security::config()->password_encryption_algorithm, $this->PasswordEncryption : Security::config()->password_encryption_algorithm,
$this $this
); );
// Overwrite the Password property with the hashed value // Overwrite the Password property with the hashed value
$this->Password = $encryption_details['password']; $this->Password = $encryption_details['password'];
$this->Salt = $encryption_details['salt']; $this->Salt = $encryption_details['salt'];
$this->PasswordEncryption = $encryption_details['algorithm']; $this->PasswordEncryption = $encryption_details['algorithm'];
// If we haven't manually set a password expiry // If we haven't manually set a password expiry
if(!$this->isChanged('PasswordExpiry')) { if (!$this->isChanged('PasswordExpiry')) {
// then set it for us // then set it for us
if(self::config()->password_expiry_days) { if (self::config()->password_expiry_days) {
$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days); $this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
} else { } else {
$this->PasswordExpiry = null; $this->PasswordExpiry = null;
} }
} }
} }
// save locale // save locale
if(!$this->Locale) { if (!$this->Locale) {
$this->Locale = i18n::get_locale(); $this->Locale = i18n::get_locale();
} }
parent::onBeforeWrite(); parent::onBeforeWrite();
} }
public function onAfterWrite() public function onAfterWrite()
{ {
parent::onAfterWrite(); parent::onAfterWrite();
Permission::flush_permission_cache(); Permission::flush_permission_cache();
if($this->isChanged('Password')) { if ($this->isChanged('Password')) {
MemberPassword::log($this); MemberPassword::log($this);
} }
} }
public function onAfterDelete() public function onAfterDelete()
{ {
parent::onAfterDelete(); parent::onAfterDelete();
//prevent orphaned records remaining in the DB //prevent orphaned records remaining in the DB
$this->deletePasswordLogs(); $this->deletePasswordLogs();
} }
/** /**
* Delete the MemberPassword objects that are associated to this user * Delete the MemberPassword objects that are associated to this user
* *
* @return $this * @return $this
*/ */
protected function deletePasswordLogs() protected function deletePasswordLogs()
{ {
foreach ($this->LoggedPasswords() as $password) { foreach ($this->LoggedPasswords() as $password) {
$password->delete(); $password->delete();
$password->destroy(); $password->destroy();
} }
return $this; return $this;
} }
/** /**
* Filter out admin groups to avoid privilege escalation, * Filter out admin groups to avoid privilege escalation,
* If any admin groups are requested, deny the whole save operation. * If any admin groups are requested, deny the whole save operation.
* *
* @param array $ids Database IDs of Group records * @param array $ids Database IDs of Group records
* @return bool True if the change can be accepted * @return bool True if the change can be accepted
*/ */
public function onChangeGroups($ids) public function onChangeGroups($ids)
{ {
// unless the current user is an admin already OR the logged in user is an admin // unless the current user is an admin already OR the logged in user is an admin
if(Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) { if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
return true; return true;
} }
// If there are no admin groups in this set then it's ok // If there are no admin groups in this set then it's ok
$adminGroups = Permission::get_groups_by_permission('ADMIN'); $adminGroups = Permission::get_groups_by_permission('ADMIN');
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array(); $adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
return count(array_intersect($ids, $adminGroupIDs)) == 0; return count(array_intersect($ids, $adminGroupIDs)) == 0;
} }
/** /**
* Check if the member is in one of the given groups. * Check if the member is in one of the given groups.
* *
* @param array|SS_List $groups Collection of {@link Group} DataObjects to check * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
* @param boolean $strict Only determine direct group membership if set to true (Default: false) * @param boolean $strict Only determine direct group membership if set to true (Default: false)
* @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE. * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
*/ */
public function inGroups($groups, $strict = false) public function inGroups($groups, $strict = false)
{ {
if ($groups) { if ($groups) {
@ -1076,608 +1076,608 @@ class Member extends DataObject implements TemplateGlobalProvider
return true; return true;
} }
} }
} }
return false; return false;
} }
/** /**
* Check if the member is in the given group or any parent groups. * Check if the member is in the given group or any parent groups.
* *
* @param int|Group|string $group Group instance, Group Code or ID * @param int|Group|string $group Group instance, Group Code or ID
* @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE) * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
* @return bool Returns TRUE if the member is in the given group, otherwise FALSE. * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
*/ */
public function inGroup($group, $strict = false) public function inGroup($group, $strict = false)
{ {
if(is_numeric($group)) { if (is_numeric($group)) {
$groupCheckObj = DataObject::get_by_id('SilverStripe\\Security\\Group', $group); $groupCheckObj = DataObject::get_by_id('SilverStripe\\Security\\Group', $group);
} elseif(is_string($group)) { } elseif (is_string($group)) {
$groupCheckObj = DataObject::get_one('SilverStripe\\Security\\Group', array( $groupCheckObj = DataObject::get_one('SilverStripe\\Security\\Group', array(
'"Group"."Code"' => $group '"Group"."Code"' => $group
)); ));
} elseif($group instanceof Group) { } elseif ($group instanceof Group) {
$groupCheckObj = $group; $groupCheckObj = $group;
} else { } else {
user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR); user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
} }
if (!$groupCheckObj) { if (!$groupCheckObj) {
return false; return false;
} }
$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups(); $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
if ($groupCandidateObjs) { if ($groupCandidateObjs) {
foreach ($groupCandidateObjs as $groupCandidateObj) { foreach ($groupCandidateObjs as $groupCandidateObj) {
if ($groupCandidateObj->ID == $groupCheckObj->ID) { if ($groupCandidateObj->ID == $groupCheckObj->ID) {
return true; return true;
} }
} }
} }
return false; return false;
} }
/** /**
* Adds the member to a group. This will create the group if the given * Adds the member to a group. This will create the group if the given
* group code does not return a valid group object. * group code does not return a valid group object.
* *
* @param string $groupcode * @param string $groupcode
* @param string $title Title of the group * @param string $title Title of the group
*/ */
public function addToGroupByCode($groupcode, $title = "") public function addToGroupByCode($groupcode, $title = "")
{ {
$group = DataObject::get_one('SilverStripe\\Security\\Group', array( $group = DataObject::get_one('SilverStripe\\Security\\Group', array(
'"Group"."Code"' => $groupcode '"Group"."Code"' => $groupcode
)); ));
if($group) { if ($group) {
$this->Groups()->add($group); $this->Groups()->add($group);
} else { } else {
if (!$title) { if (!$title) {
$title = $groupcode; $title = $groupcode;
} }
$group = new Group(); $group = new Group();
$group->Code = $groupcode; $group->Code = $groupcode;
$group->Title = $title; $group->Title = $title;
$group->write(); $group->write();
$this->Groups()->add($group); $this->Groups()->add($group);
} }
} }
/** /**
* Removes a member from a group. * Removes a member from a group.
* *
* @param string $groupcode * @param string $groupcode
*/ */
public function removeFromGroupByCode($groupcode) public function removeFromGroupByCode($groupcode)
{ {
$group = Group::get()->filter(array('Code' => $groupcode))->first(); $group = Group::get()->filter(array('Code' => $groupcode))->first();
if($group) { if ($group) {
$this->Groups()->remove($group); $this->Groups()->remove($group);
} }
} }
/** /**
* @param array $columns Column names on the Member record to show in {@link getTitle()}. * @param array $columns Column names on the Member record to show in {@link getTitle()}.
* @param String $sep Separator * @param String $sep Separator
*/ */
public static function set_title_columns($columns, $sep = ' ') public static function set_title_columns($columns, $sep = ' ')
{ {
if (!is_array($columns)) { if (!is_array($columns)) {
$columns = array($columns); $columns = array($columns);
} }
self::config()->title_format = array('columns' => $columns, 'sep' => $sep); self::config()->title_format = array('columns' => $columns, 'sep' => $sep);
} }
//------------------- HELPER METHODS -----------------------------------// //------------------- HELPER METHODS -----------------------------------//
/** /**
* Get the complete name of the member, by default in the format "<Surname>, <FirstName>". * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
* Falls back to showing either field on its own. * Falls back to showing either field on its own.
* *
* You can overload this getter with {@link set_title_format()} * You can overload this getter with {@link set_title_format()}
* and {@link set_title_sql()}. * and {@link set_title_sql()}.
* *
* @return string Returns the first- and surname of the member. If the ID * @return string Returns the first- and surname of the member. If the ID
* of the member is equal 0, only the surname is returned. * of the member is equal 0, only the surname is returned.
*/ */
public function getTitle() public function getTitle()
{ {
$format = $this->config()->title_format; $format = $this->config()->title_format;
if ($format) { if ($format) {
$values = array(); $values = array();
foreach($format['columns'] as $col) { foreach ($format['columns'] as $col) {
$values[] = $this->getField($col); $values[] = $this->getField($col);
} }
return join($format['sep'], $values); return join($format['sep'], $values);
} }
if ($this->getField('ID') === 0) { if ($this->getField('ID') === 0) {
return $this->getField('Surname'); return $this->getField('Surname');
} else { } else {
if($this->getField('Surname') && $this->getField('FirstName')){ if ($this->getField('Surname') && $this->getField('FirstName')) {
return $this->getField('Surname') . ', ' . $this->getField('FirstName'); return $this->getField('Surname') . ', ' . $this->getField('FirstName');
}elseif($this->getField('Surname')){ } elseif ($this->getField('Surname')) {
return $this->getField('Surname'); return $this->getField('Surname');
}elseif($this->getField('FirstName')){ } elseif ($this->getField('FirstName')) {
return $this->getField('FirstName'); return $this->getField('FirstName');
}else{ } else {
return null; return null;
} }
} }
} }
/** /**
* Return a SQL CONCAT() fragment suitable for a SELECT statement. * Return a SQL CONCAT() fragment suitable for a SELECT statement.
* Useful for custom queries which assume a certain member title format. * Useful for custom queries which assume a certain member title format.
* *
* @return String SQL * @return String SQL
*/ */
public static function get_title_sql() public static function get_title_sql()
{ {
// This should be abstracted to SSDatabase concatOperator or similar. // This should be abstracted to SSDatabase concatOperator or similar.
$op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || "; $op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
// Get title_format with fallback to default // Get title_format with fallback to default
$format = static::config()->title_format; $format = static::config()->title_format;
if (!$format) { if (!$format) {
$format = [ $format = [
'columns' => ['Surname', 'FirstName'], 'columns' => ['Surname', 'FirstName'],
'sep' => ' ', 'sep' => ' ',
]; ];
} }
$columnsWithTablename = array(); $columnsWithTablename = array();
foreach($format['columns'] as $column) { foreach ($format['columns'] as $column) {
$columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column); $columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
} }
$sepSQL = Convert::raw2sql($format['sep'], true); $sepSQL = Convert::raw2sql($format['sep'], true);
return "(".join(" $op $sepSQL $op ", $columnsWithTablename).")"; return "(".join(" $op $sepSQL $op ", $columnsWithTablename).")";
} }
/** /**
* Get the complete name of the member * Get the complete name of the member
* *
* @return string Returns the first- and surname of the member. * @return string Returns the first- and surname of the member.
*/ */
public function getName() public function getName()
{ {
return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName; return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
} }
/** /**
* Set first- and surname * Set first- and surname
* *
* This method assumes that the last part of the name is the surname, e.g. * This method assumes that the last part of the name is the surname, e.g.
* <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i> * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
* *
* @param string $name The name * @param string $name The name
*/ */
public function setName($name) public function setName($name)
{ {
$nameParts = explode(' ', $name); $nameParts = explode(' ', $name);
$this->Surname = array_pop($nameParts); $this->Surname = array_pop($nameParts);
$this->FirstName = join(' ', $nameParts); $this->FirstName = join(' ', $nameParts);
} }
/** /**
* Alias for {@link setName} * Alias for {@link setName}
* *
* @param string $name The name * @param string $name The name
* @see setName() * @see setName()
*/ */
public function splitName($name) public function splitName($name)
{ {
return $this->setName($name); return $this->setName($name);
} }
/** /**
* Override the default getter for DateFormat so the * Override the default getter for DateFormat so the
* default format for the user's locale is used * default format for the user's locale is used
* if the user has not defined their own. * if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getDateFormat() public function getDateFormat()
{ {
if($this->getField('DateFormat')) { if ($this->getField('DateFormat')) {
return $this->getField('DateFormat'); return $this->getField('DateFormat');
} else { } else {
return i18n::config()->get('date_format'); return i18n::config()->get('date_format');
} }
} }
/** /**
* Override the default getter for TimeFormat so the * Override the default getter for TimeFormat so the
* default format for the user's locale is used * default format for the user's locale is used
* if the user has not defined their own. * if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getTimeFormat() public function getTimeFormat()
{ {
if($this->getField('TimeFormat')) { if ($this->getField('TimeFormat')) {
return $this->getField('TimeFormat'); return $this->getField('TimeFormat');
} else { } else {
return i18n::config()->get('time_format'); return i18n::config()->get('time_format');
} }
} }
//---------------------------------------------------------------------// //---------------------------------------------------------------------//
/** /**
* Get a "many-to-many" map that holds for all members their group memberships, * Get a "many-to-many" map that holds for all members their group memberships,
* including any parent groups where membership is implied. * including any parent groups where membership is implied.
* Use {@link DirectGroups()} to only retrieve the group relations without inheritance. * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
* *
* @todo Push all this logic into Member_GroupSet's getIterator()? * @todo Push all this logic into Member_GroupSet's getIterator()?
* @return Member_Groupset * @return Member_Groupset
*/ */
public function Groups() public function Groups()
{ {
$groups = Member_GroupSet::create('SilverStripe\\Security\\Group', 'Group_Members', 'GroupID', 'MemberID'); $groups = Member_GroupSet::create('SilverStripe\\Security\\Group', 'Group_Members', 'GroupID', 'MemberID');
$groups = $groups->forForeignID($this->ID); $groups = $groups->forForeignID($this->ID);
$this->extend('updateGroups', $groups); $this->extend('updateGroups', $groups);
return $groups; return $groups;
} }
/** /**
* @return ManyManyList * @return ManyManyList
*/ */
public function DirectGroups() public function DirectGroups()
{ {
return $this->getManyManyComponents('Groups'); return $this->getManyManyComponents('Groups');
} }
/** /**
* Get a member SQLMap of members in specific groups * Get a member SQLMap of members in specific groups
* *
* If no $groups is passed, all members will be returned * If no $groups is passed, all members will be returned
* *
* @param mixed $groups - takes a SS_List, an array or a single Group.ID * @param mixed $groups - takes a SS_List, an array or a single Group.ID
* @return Map Returns an Map that returns all Member data. * @return Map Returns an Map that returns all Member data.
*/ */
public static function map_in_groups($groups = null) public static function map_in_groups($groups = null)
{ {
$groupIDList = array(); $groupIDList = array();
if($groups instanceof SS_List) { if ($groups instanceof SS_List) {
foreach( $groups as $group ) { foreach ($groups as $group) {
$groupIDList[] = $group->ID; $groupIDList[] = $group->ID;
} }
} elseif(is_array($groups)) { } elseif (is_array($groups)) {
$groupIDList = $groups; $groupIDList = $groups;
} elseif($groups) { } elseif ($groups) {
$groupIDList[] = $groups; $groupIDList[] = $groups;
} }
// No groups, return all Members // No groups, return all Members
if(!$groupIDList) { if (!$groupIDList) {
return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map(); return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
} }
$membersList = new ArrayList(); $membersList = new ArrayList();
// This is a bit ineffective, but follow the ORM style // This is a bit ineffective, but follow the ORM style
foreach(Group::get()->byIDs($groupIDList) as $group) { foreach (Group::get()->byIDs($groupIDList) as $group) {
$membersList->merge($group->Members()); $membersList->merge($group->Members());
} }
$membersList->removeDuplicates('ID'); $membersList->removeDuplicates('ID');
return $membersList->map(); return $membersList->map();
} }
/** /**
* Get a map of all members in the groups given that have CMS permissions * Get a map of all members in the groups given that have CMS permissions
* *
* If no groups are passed, all groups with CMS permissions will be used. * If no groups are passed, all groups with CMS permissions will be used.
* *
* @param array $groups Groups to consider or NULL to use all groups with * @param array $groups Groups to consider or NULL to use all groups with
* CMS permissions. * CMS permissions.
* @return Map Returns a map of all members in the groups given that * @return Map Returns a map of all members in the groups given that
* have CMS permissions. * have CMS permissions.
*/ */
public static function mapInCMSGroups($groups = null) public static function mapInCMSGroups($groups = null)
{ {
if(!$groups || $groups->Count() == 0) { if (!$groups || $groups->Count() == 0) {
$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin'); $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
if (class_exists('SilverStripe\\CMS\\Controllers\\CMSMain')) { if (class_exists('SilverStripe\\CMS\\Controllers\\CMSMain')) {
$cmsPerms = CMSMain::singleton()->providePermissions(); $cmsPerms = CMSMain::singleton()->providePermissions();
} else { } else {
$cmsPerms = LeftAndMain::singleton()->providePermissions(); $cmsPerms = LeftAndMain::singleton()->providePermissions();
} }
if(!empty($cmsPerms)) { if (!empty($cmsPerms)) {
$perms = array_unique(array_merge($perms, array_keys($cmsPerms))); $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
} }
$permsClause = DB::placeholders($perms); $permsClause = DB::placeholders($perms);
/** @skipUpgrade */ /** @skipUpgrade */
$groups = Group::get() $groups = Group::get()
->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"') ->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
->where(array( ->where(array(
"\"Permission\".\"Code\" IN ($permsClause)" => $perms "\"Permission\".\"Code\" IN ($permsClause)" => $perms
)); ));
} }
$groupIDList = array(); $groupIDList = array();
if($groups instanceof SS_List) { if ($groups instanceof SS_List) {
foreach($groups as $group) { foreach ($groups as $group) {
$groupIDList[] = $group->ID; $groupIDList[] = $group->ID;
} }
} elseif(is_array($groups)) { } elseif (is_array($groups)) {
$groupIDList = $groups; $groupIDList = $groups;
} }
/** @skipUpgrade */ /** @skipUpgrade */
$members = Member::get() $members = Member::get()
->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"') ->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"'); ->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
if($groupIDList) { if ($groupIDList) {
$groupClause = DB::placeholders($groupIDList); $groupClause = DB::placeholders($groupIDList);
$members = $members->where(array( $members = $members->where(array(
"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList "\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
)); ));
} }
return $members->sort('"Member"."Surname", "Member"."FirstName"')->map(); return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
} }
/** /**
* Get the groups in which the member is NOT in * Get the groups in which the member is NOT in
* *
* When passed an array of groups, and a component set of groups, this * When passed an array of groups, and a component set of groups, this
* function will return the array of groups the member is NOT in. * function will return the array of groups the member is NOT in.
* *
* @param array $groupList An array of group code names. * @param array $groupList An array of group code names.
* @param array $memberGroups A component set of groups (if set to NULL, * @param array $memberGroups A component set of groups (if set to NULL,
* $this->groups() will be used) * $this->groups() will be used)
* @return array Groups in which the member is NOT in. * @return array Groups in which the member is NOT in.
*/ */
public function memberNotInGroups($groupList, $memberGroups = null) public function memberNotInGroups($groupList, $memberGroups = null)
{ {
if (!$memberGroups) { if (!$memberGroups) {
$memberGroups = $this->Groups(); $memberGroups = $this->Groups();
} }
foreach($memberGroups as $group) { foreach ($memberGroups as $group) {
if(in_array($group->Code, $groupList)) { if (in_array($group->Code, $groupList)) {
$index = array_search($group->Code, $groupList); $index = array_search($group->Code, $groupList);
unset($groupList[$index]); unset($groupList[$index]);
} }
} }
return $groupList; return $groupList;
} }
/** /**
* Return a {@link FieldList} of fields that would appropriate for editing * Return a {@link FieldList} of fields that would appropriate for editing
* this member. * this member.
* *
* @return FieldList Return a FieldList of fields that would appropriate for * @return FieldList Return a FieldList of fields that would appropriate for
* editing this member. * editing this member.
*/ */
public function getCMSFields() public function getCMSFields()
{ {
require_once 'Zend/Date.php'; require_once 'Zend/Date.php';
$self = $this; $self = $this;
$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) { $this->beforeUpdateCMSFields(function (FieldList $fields) use ($self) {
/** @var FieldList $mainFields */ /** @var FieldList $mainFields */
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren(); $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
// Build change password field // Build change password field
$mainFields->replaceField('Password', $self->getMemberPasswordField()); $mainFields->replaceField('Password', $self->getMemberPasswordField());
$mainFields->replaceField('Locale', new DropdownField( $mainFields->replaceField('Locale', new DropdownField(
"Locale", "Locale",
_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'), _t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
i18n::get_existing_translations() i18n::get_existing_translations()
)); ));
$mainFields->removeByName($self->config()->hidden_fields); $mainFields->removeByName($self->config()->hidden_fields);
if( ! $self->config()->lock_out_after_incorrect_logins) { if (! $self->config()->lock_out_after_incorrect_logins) {
$mainFields->removeByName('FailedLoginCount'); $mainFields->removeByName('FailedLoginCount');
} }
// Groups relation will get us into logical conflicts because // Groups relation will get us into logical conflicts because
// Members are displayed within group edit form in SecurityAdmin // Members are displayed within group edit form in SecurityAdmin
$fields->removeByName('Groups'); $fields->removeByName('Groups');
// Members shouldn't be able to directly view/edit logged passwords // Members shouldn't be able to directly view/edit logged passwords
$fields->removeByName('LoggedPasswords'); $fields->removeByName('LoggedPasswords');
$fields->removeByName('RememberLoginHashes'); $fields->removeByName('RememberLoginHashes');
if(Permission::check('EDIT_PERMISSIONS')) { if (Permission::check('EDIT_PERMISSIONS')) {
$groupsMap = array(); $groupsMap = array();
foreach(Group::get() as $group) { foreach (Group::get() as $group) {
// Listboxfield values are escaped, use ASCII char instead of &raquo; // Listboxfield values are escaped, use ASCII char instead of &raquo;
$groupsMap[$group->ID] = $group->getBreadcrumbs(' > '); $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
} }
asort($groupsMap); asort($groupsMap);
$fields->addFieldToTab( $fields->addFieldToTab(
'Root.Main', 'Root.Main',
ListboxField::create('DirectGroups', singleton('SilverStripe\\Security\\Group')->i18n_plural_name()) ListboxField::create('DirectGroups', singleton('SilverStripe\\Security\\Group')->i18n_plural_name())
->setSource($groupsMap) ->setSource($groupsMap)
->setAttribute( ->setAttribute(
'data-placeholder', 'data-placeholder',
_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown') _t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
) )
); );
// Add permission field (readonly to avoid complicated group assignment logic). // Add permission field (readonly to avoid complicated group assignment logic).
// This should only be available for existing records, as new records start // This should only be available for existing records, as new records start
// with no permissions until they have a group assignment anyway. // with no permissions until they have a group assignment anyway.
if($self->ID) { if ($self->ID) {
$permissionsField = new PermissionCheckboxSetField_Readonly( $permissionsField = new PermissionCheckboxSetField_Readonly(
'Permissions', 'Permissions',
false, false,
'SilverStripe\\Security\\Permission', 'SilverStripe\\Security\\Permission',
'GroupID', 'GroupID',
// we don't want parent relationships, they're automatically resolved in the field // we don't want parent relationships, they're automatically resolved in the field
$self->getManyManyComponents('Groups') $self->getManyManyComponents('Groups')
); );
$fields->findOrMakeTab('Root.Permissions', singleton('SilverStripe\\Security\\Permission')->i18n_plural_name()); $fields->findOrMakeTab('Root.Permissions', singleton('SilverStripe\\Security\\Permission')->i18n_plural_name());
$fields->addFieldToTab('Root.Permissions', $permissionsField); $fields->addFieldToTab('Root.Permissions', $permissionsField);
} }
} }
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions'); $permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
if ($permissionsTab) { if ($permissionsTab) {
$permissionsTab->addExtraClass('readonly'); $permissionsTab->addExtraClass('readonly');
} }
$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale)); $defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
$dateFormatMap = array( $dateFormatMap = array(
'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'), 'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'), 'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'), 'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'), 'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
); );
$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat) $dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); . sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
$mainFields->push( $mainFields->push(
$dateFormatField = new MemberDatetimeOptionsetField( $dateFormatField = new MemberDatetimeOptionsetField(
'DateFormat', 'DateFormat',
$self->fieldLabel('DateFormat'), $self->fieldLabel('DateFormat'),
$dateFormatMap $dateFormatMap
) )
); );
$formatClass = get_class($dateFormatField); $formatClass = get_class($dateFormatField);
$dateFormatField->setValue($self->DateFormat); $dateFormatField->setValue($self->DateFormat);
$dateTemplate = SSViewer::get_templates_by_class($formatClass, '_description_date', $formatClass); $dateTemplate = SSViewer::get_templates_by_class($formatClass, '_description_date', $formatClass);
$dateFormatField->setDescriptionTemplate($dateTemplate); $dateFormatField->setDescriptionTemplate($dateTemplate);
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale)); $defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
$timeFormatMap = array( $timeFormatMap = array(
'h:mm a' => Zend_Date::now()->toString('h:mm a'), 'h:mm a' => Zend_Date::now()->toString('h:mm a'),
'H:mm' => Zend_Date::now()->toString('H:mm'), 'H:mm' => Zend_Date::now()->toString('H:mm'),
); );
$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat) $timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); . sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
$mainFields->push( $mainFields->push(
$timeFormatField = new MemberDatetimeOptionsetField( $timeFormatField = new MemberDatetimeOptionsetField(
'TimeFormat', 'TimeFormat',
$self->fieldLabel('TimeFormat'), $self->fieldLabel('TimeFormat'),
$timeFormatMap $timeFormatMap
) )
); );
$timeFormatField->setValue($self->TimeFormat); $timeFormatField->setValue($self->TimeFormat);
$timeTemplate = SSViewer::get_templates_by_class($formatClass,'_description_time', $formatClass); $timeTemplate = SSViewer::get_templates_by_class($formatClass, '_description_time', $formatClass);
$timeFormatField->setDescriptionTemplate($timeTemplate); $timeFormatField->setDescriptionTemplate($timeTemplate);
}); });
return parent::getCMSFields(); return parent::getCMSFields();
} }
/** /**
* @param bool $includerelations Indicate if the labels returned include relation fields * @param bool $includerelations Indicate if the labels returned include relation fields
* @return array * @return array
*/ */
public function fieldLabels($includerelations = true) public function fieldLabels($includerelations = true)
{ {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includerelations);
$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name'); $labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
$labels['Surname'] = _t('Member.SURNAME', 'Surname'); $labels['Surname'] = _t('Member.SURNAME', 'Surname');
/** @skipUpgrade */ /** @skipUpgrade */
$labels['Email'] = _t('Member.EMAIL', 'Email'); $labels['Email'] = _t('Member.EMAIL', 'Email');
$labels['Password'] = _t('Member.db_Password', 'Password'); $labels['Password'] = _t('Member.db_Password', 'Password');
$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale'); $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format'); $labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format'); $labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
if($includerelations){ if ($includerelations) {
$labels['Groups'] = _t( $labels['Groups'] = _t(
'Member.belongs_many_many_Groups', 'Member.belongs_many_many_Groups',
'Groups', 'Groups',
'Security Groups this member belongs to' 'Security Groups this member belongs to'
); );
} }
return $labels; return $labels;
} }
/** /**
* Users can view their own record. * Users can view their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions. * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
* This is likely to be customized for social sites etc. with a looser permission model. * This is likely to be customized for social sites etc. with a looser permission model.
* *
* @param Member $member * @param Member $member
* @return bool * @return bool
*/ */
public function canView($member = null) public function canView($member = null)
{ {
//get member //get member
if(!($member instanceof Member)) { if (!($member instanceof Member)) {
$member = Member::currentUser(); $member = Member::currentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
if($extended !== null) { if ($extended !== null) {
return $extended; return $extended;
} }
//need to be logged in and/or most checks below rely on $member being a Member //need to be logged in and/or most checks below rely on $member being a Member
if(!$member) { if (!$member) {
return false; return false;
} }
// members can usually view their own record // members can usually view their own record
if($this->ID == $member->ID) { if ($this->ID == $member->ID) {
return true; return true;
} }
//standard check //standard check
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
} }
/** /**
* Users can edit their own record. * Users can edit their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
* *
* @param Member $member * @param Member $member
* @return bool * @return bool
*/ */
public function canEdit($member = null) public function canEdit($member = null)
{ {
//get member //get member
if(!($member instanceof Member)) { if (!($member instanceof Member)) {
$member = Member::currentUser(); $member = Member::currentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
if($extended !== null) { if ($extended !== null) {
return $extended; return $extended;
} }
//need to be logged in and/or most checks below rely on $member being a Member //need to be logged in and/or most checks below rely on $member being a Member
if(!$member) { if (!$member) {
return false; return false;
} }
// HACK: we should not allow for an non-Admin to edit an Admin // HACK: we should not allow for an non-Admin to edit an Admin
if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) { if (!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
return false; return false;
} }
// members can usually edit their own record // members can usually edit their own record
if($this->ID == $member->ID) { if ($this->ID == $member->ID) {
return true; return true;
} }
//standard check //standard check
@ -1686,36 +1686,36 @@ class Member extends DataObject implements TemplateGlobalProvider
/** /**
* Users can edit their own record. * Users can edit their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
* *
* @param Member $member * @param Member $member
* @return bool * @return bool
*/ */
public function canDelete($member = null) public function canDelete($member = null)
{ {
if(!($member instanceof Member)) { if (!($member instanceof Member)) {
$member = Member::currentUser(); $member = Member::currentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
if($extended !== null) { if ($extended !== null) {
return $extended; return $extended;
} }
//need to be logged in and/or most checks below rely on $member being a Member //need to be logged in and/or most checks below rely on $member being a Member
if(!$member) { if (!$member) {
return false; return false;
} }
// Members are not allowed to remove themselves, // Members are not allowed to remove themselves,
// since it would create inconsistencies in the admin UIs. // since it would create inconsistencies in the admin UIs.
if($this->ID && $member->ID == $this->ID) { if ($this->ID && $member->ID == $this->ID) {
return false; return false;
} }
// HACK: if you want to delete a member, you have to be a member yourself. // HACK: if you want to delete a member, you have to be a member yourself.
// this is a hack because what this should do is to stop a user // this is a hack because what this should do is to stop a user
// deleting a member who has more privileges (e.g. a non-Admin deleting an Admin) // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
if(Permission::checkMember($this, 'ADMIN')) { if (Permission::checkMember($this, 'ADMIN')) {
if( ! Permission::checkMember($member, 'ADMIN')) { if (! Permission::checkMember($member, 'ADMIN')) {
return false; return false;
} }
} }
@ -1723,111 +1723,111 @@ class Member extends DataObject implements TemplateGlobalProvider
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
} }
/** /**
* Validate this member object. * Validate this member object.
*/ */
public function validate() public function validate()
{ {
$valid = parent::validate(); $valid = parent::validate();
if(!$this->ID || $this->isChanged('Password')) { if (!$this->ID || $this->isChanged('Password')) {
if($this->Password && self::$password_validator) { if ($this->Password && self::$password_validator) {
$valid->combineAnd(self::$password_validator->validate($this->Password, $this)); $valid->combineAnd(self::$password_validator->validate($this->Password, $this));
} }
} }
if((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) { if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
if($this->SetPassword && self::$password_validator) { if ($this->SetPassword && self::$password_validator) {
$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this)); $valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this));
} }
} }
return $valid; return $valid;
} }
/** /**
* Change password. This will cause rehashing according to * Change password. This will cause rehashing according to
* the `PasswordEncryption` property. * the `PasswordEncryption` property.
* *
* @param string $password Cleartext password * @param string $password Cleartext password
* @return ValidationResult * @return ValidationResult
*/ */
public function changePassword($password) public function changePassword($password)
{ {
$this->Password = $password; $this->Password = $password;
$valid = $this->validate(); $valid = $this->validate();
if($valid->valid()) { if ($valid->isValid()) {
$this->AutoLoginHash = null; $this->AutoLoginHash = null;
$this->write(); $this->write();
} }
return $valid; return $valid;
} }
/** /**
* Tell this member that someone made a failed attempt at logging in as them. * Tell this member that someone made a failed attempt at logging in as them.
* This can be used to lock the user out temporarily if too many failed attempts are made. * This can be used to lock the user out temporarily if too many failed attempts are made.
*/ */
public function registerFailedLogin() public function registerFailedLogin()
{ {
if(self::config()->lock_out_after_incorrect_logins) { if (self::config()->lock_out_after_incorrect_logins) {
// Keep a tally of the number of failed log-ins so that we can lock people out // Keep a tally of the number of failed log-ins so that we can lock people out
$this->FailedLoginCount = $this->FailedLoginCount + 1; $this->FailedLoginCount = $this->FailedLoginCount + 1;
if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) { if ($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
$lockoutMins = self::config()->lock_out_delay_mins; $lockoutMins = self::config()->lock_out_delay_mins;
$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->Format('U') + $lockoutMins*60); $this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->Format('U') + $lockoutMins*60);
$this->FailedLoginCount = 0; $this->FailedLoginCount = 0;
} }
} }
$this->extend('registerFailedLogin'); $this->extend('registerFailedLogin');
$this->write(); $this->write();
} }
/** /**
* Tell this member that a successful login has been made * Tell this member that a successful login has been made
*/ */
public function registerSuccessfulLogin() public function registerSuccessfulLogin()
{ {
if(self::config()->lock_out_after_incorrect_logins) { if (self::config()->lock_out_after_incorrect_logins) {
// Forgive all past login failures // Forgive all past login failures
$this->FailedLoginCount = 0; $this->FailedLoginCount = 0;
$this->write(); $this->write();
} }
} }
/** /**
* Get the HtmlEditorConfig for this user to be used in the CMS. * Get the HtmlEditorConfig for this user to be used in the CMS.
* This is set by the group. If multiple configurations are set, * This is set by the group. If multiple configurations are set,
* the one with the highest priority wins. * the one with the highest priority wins.
* *
* @return string * @return string
*/ */
public function getHtmlEditorConfigForCMS() public function getHtmlEditorConfigForCMS()
{ {
$currentName = ''; $currentName = '';
$currentPriority = 0; $currentPriority = 0;
foreach($this->Groups() as $group) { foreach ($this->Groups() as $group) {
$configName = $group->HtmlEditorConfig; $configName = $group->HtmlEditorConfig;
if($configName) { if ($configName) {
$config = HTMLEditorConfig::get($group->HtmlEditorConfig); $config = HTMLEditorConfig::get($group->HtmlEditorConfig);
if($config && $config->getOption('priority') > $currentPriority) { if ($config && $config->getOption('priority') > $currentPriority) {
$currentName = $configName; $currentName = $configName;
$currentPriority = $config->getOption('priority'); $currentPriority = $config->getOption('priority');
} }
} }
} }
// If can't find a suitable editor, just default to cms // If can't find a suitable editor, just default to cms
return $currentName ? $currentName : 'cms'; return $currentName ? $currentName : 'cms';
} }
public static function get_template_global_variables() public static function get_template_global_variables()
{ {
return array( return array(
'CurrentMember' => 'currentUser', 'CurrentMember' => 'currentUser',
'currentUser', 'currentUser',
); );
} }
} }

View File

@ -16,208 +16,212 @@ use InvalidArgumentException;
class MemberAuthenticator extends Authenticator class MemberAuthenticator extends Authenticator
{ {
/** /**
* Contains encryption algorithm identifiers. * Contains encryption algorithm identifiers.
* If set, will migrate to new precision-safe password hashing * If set, will migrate to new precision-safe password hashing
* upon login. See http://open.silverstripe.org/ticket/3004 * upon login. See http://open.silverstripe.org/ticket/3004
* *
* @var array * @var array
*/ */
private static $migrate_legacy_hashes = array( private static $migrate_legacy_hashes = array(
'md5' => 'md5_v2.4', 'md5' => 'md5_v2.4',
'sha1' => 'sha1_v2.4' 'sha1' => 'sha1_v2.4'
); );
/** /**
* Attempt to find and authenticate member if possible from the given data * Attempt to find and authenticate member if possible from the given data
* *
* @param array $data * @param array $data
* @param Form $form * @param Form $form
* @param bool &$success Success flag * @param bool &$success Success flag
* @return Member Found member, regardless of successful login * @return Member Found member, regardless of successful login
*/ */
protected static function authenticate_member($data, $form, &$success) protected static function authenticate_member($data, $form, &$success)
{ {
// Default success to false // Default success to false
$success = false; $success = false;
// Attempt to identify by temporary ID // Attempt to identify by temporary ID
$member = null; $member = null;
$email = null; $email = null;
if(!empty($data['tempid'])) { if (!empty($data['tempid'])) {
// Find user by tempid, in case they are re-validating an existing session // Find user by tempid, in case they are re-validating an existing session
$member = Member::member_from_tempid($data['tempid']); $member = Member::member_from_tempid($data['tempid']);
if ($member) { if ($member) {
$email = $member->Email; $email = $member->Email;
} }
} }
// Otherwise, get email from posted value instead // Otherwise, get email from posted value instead
/** @skipUpgrade */ /** @skipUpgrade */
if(!$member && !empty($data['Email'])) { if (!$member && !empty($data['Email'])) {
$email = $data['Email']; $email = $data['Email'];
} }
// Check default login (see Security::setDefaultAdmin()) // Check default login (see Security::setDefaultAdmin())
$asDefaultAdmin = $email === Security::default_admin_username(); $asDefaultAdmin = $email === Security::default_admin_username();
if($asDefaultAdmin) { if ($asDefaultAdmin) {
// If logging is as default admin, ensure record is setup correctly // If logging is as default admin, ensure record is setup correctly
$member = Member::default_admin(); $member = Member::default_admin();
$success = !$member->isLockedOut() && Security::check_default_admin($email, $data['Password']); $success = !$member->isLockedOut() && Security::check_default_admin($email, $data['Password']);
//protect against failed login //protect against failed login
if($success) { if ($success) {
return $member; return $member;
} }
} }
// Attempt to identify user by email // Attempt to identify user by email
if(!$member && $email) { if (!$member && $email) {
// Find user by email // Find user by email
$member = Member::get() $member = Member::get()
->filter(Member::config()->unique_identifier_field, $email) ->filter(Member::config()->unique_identifier_field, $email)
->first(); ->first();
} }
// Validate against member if possible // Validate against member if possible
if($member && !$asDefaultAdmin) { if ($member && !$asDefaultAdmin) {
$result = $member->checkPassword($data['Password']); $result = $member->checkPassword($data['Password']);
$success = $result->valid(); $success = $result->isValid();
} else { } else {
$result = ValidationResult::create()->addError(_t('Member.ERRORWRONGCRED')); $result = ValidationResult::create()->addError(_t('Member.ERRORWRONGCRED'));
} }
// Emit failure to member and form (if available) // Emit failure to member and form (if available)
if(!$success) { if (!$success) {
if($member) $member->registerFailedLogin(); if ($member) {
if($form) $form->setSessionValidationResult($result, true); $member->registerFailedLogin();
} else { }
if ($form) {
$form->setSessionValidationResult($result, true);
}
} else {
if ($member) { if ($member) {
$member->registerSuccessfulLogin(); $member->registerSuccessfulLogin();
} }
} }
return $member; return $member;
} }
/** /**
* Log login attempt * Log login attempt
* TODO We could handle this with an extension * TODO We could handle this with an extension
* *
* @param array $data * @param array $data
* @param Member $member * @param Member $member
* @param bool $success * @param bool $success
*/ */
protected static function record_login_attempt($data, $member, $success) protected static function record_login_attempt($data, $member, $success)
{ {
if (!Security::config()->login_recording) { if (!Security::config()->login_recording) {
return; return;
} }
// Check email is valid // Check email is valid
/** @skipUpgrade */ /** @skipUpgrade */
$email = isset($data['Email']) ? $data['Email'] : null; $email = isset($data['Email']) ? $data['Email'] : null;
if(is_array($email)) { if (is_array($email)) {
throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email"); throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
} }
$attempt = new LoginAttempt(); $attempt = new LoginAttempt();
if($success) { if ($success) {
// successful login (member is existing with matching password) // successful login (member is existing with matching password)
$attempt->MemberID = $member->ID; $attempt->MemberID = $member->ID;
$attempt->Status = 'Success'; $attempt->Status = 'Success';
// Audit logging hook // Audit logging hook
$member->extend('authenticated'); $member->extend('authenticated');
} else { } else {
// Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords) // Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
$attempt->Status = 'Failure'; $attempt->Status = 'Failure';
if($member) { if ($member) {
// Audit logging hook // Audit logging hook
$attempt->MemberID = $member->ID; $attempt->MemberID = $member->ID;
$member->extend('authenticationFailed'); $member->extend('authenticationFailed');
} else { } else {
// Audit logging hook // Audit logging hook
Member::singleton()->extend('authenticationFailedUnknownUser', $data); Member::singleton()->extend('authenticationFailedUnknownUser', $data);
} }
} }
$attempt->Email = $email; $attempt->Email = $email;
$attempt->IP = Controller::curr()->getRequest()->getIP(); $attempt->IP = Controller::curr()->getRequest()->getIP();
$attempt->write(); $attempt->write();
} }
/** /**
* Method to authenticate an user * Method to authenticate an user
* *
* @param array $data Raw data to authenticate the user * @param array $data Raw data to authenticate the user
* @param Form $form Optional: If passed, better error messages can be * @param Form $form Optional: If passed, better error messages can be
* produced by using * produced by using
* {@link Form::sessionMessage()} * {@link Form::sessionMessage()}
* @return bool|Member Returns FALSE if authentication fails, otherwise * @return bool|Member Returns FALSE if authentication fails, otherwise
* the member object * the member object
* @see Security::setDefaultAdmin() * @see Security::setDefaultAdmin()
*/ */
public static function authenticate($data, Form $form = null) public static function authenticate($data, Form $form = null)
{ {
// Find authenticated member // Find authenticated member
$member = static::authenticate_member($data, $form, $success); $member = static::authenticate_member($data, $form, $success);
// Optionally record every login attempt as a {@link LoginAttempt} object // Optionally record every login attempt as a {@link LoginAttempt} object
static::record_login_attempt($data, $member, $success); static::record_login_attempt($data, $member, $success);
// Legacy migration to precision-safe password hashes. // Legacy migration to precision-safe password hashes.
// A login-event with cleartext passwords is the only time // A login-event with cleartext passwords is the only time
// when we can rehash passwords to a different hashing algorithm, // when we can rehash passwords to a different hashing algorithm,
// bulk-migration doesn't work due to the nature of hashing. // bulk-migration doesn't work due to the nature of hashing.
// See PasswordEncryptor_LegacyPHPHash class. // See PasswordEncryptor_LegacyPHPHash class.
if($success && $member && isset(self::$migrate_legacy_hashes[$member->PasswordEncryption])) { if ($success && $member && isset(self::$migrate_legacy_hashes[$member->PasswordEncryption])) {
$member->Password = $data['Password']; $member->Password = $data['Password'];
$member->PasswordEncryption = self::$migrate_legacy_hashes[$member->PasswordEncryption]; $member->PasswordEncryption = self::$migrate_legacy_hashes[$member->PasswordEncryption];
$member->write(); $member->write();
} }
if ($success) { if ($success) {
Session::clear('BackURL'); Session::clear('BackURL');
} }
return $success ? $member : null; return $success ? $member : null;
} }
/** /**
* Method that creates the login form for this authentication method * Method that creates the login form for this authentication method
* *
* @param Controller $controller The parent controller, necessary to create the * @param Controller $controller The parent controller, necessary to create the
* appropriate form action tag * appropriate form action tag
* @return Form Returns the login form to use with this authentication * @return Form Returns the login form to use with this authentication
* method * method
*/ */
public static function get_login_form(Controller $controller) public static function get_login_form(Controller $controller)
{ {
/** @skipUpgrade */ /** @skipUpgrade */
return MemberLoginForm::create($controller, "LoginForm"); return MemberLoginForm::create($controller, "LoginForm");
} }
public static function get_cms_login_form(Controller $controller) public static function get_cms_login_form(Controller $controller)
{ {
/** @skipUpgrade */ /** @skipUpgrade */
return CMSMemberLoginForm::create($controller, "LoginForm"); return CMSMemberLoginForm::create($controller, "LoginForm");
} }
public static function supports_cms() public static function supports_cms()
{ {
// Don't automatically support subclasses of MemberAuthenticator // Don't automatically support subclasses of MemberAuthenticator
return get_called_class() === __CLASS__; return get_called_class() === __CLASS__;
} }
/** /**
* Get the name of the authentication method * Get the name of the authentication method
* *
* @return string Returns the name of the authentication method. * @return string Returns the name of the authentication method.
*/ */
public static function get_name() public static function get_name()
{ {
return _t('MemberAuthenticator.TITLE', "E-mail &amp; Password"); return _t('MemberAuthenticator.TITLE', "E-mail &amp; Password");
} }
} }

View File

@ -8,6 +8,7 @@ use SilverStripe\Control\Director;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Dev\Debug;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
@ -16,6 +17,7 @@ use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
/** /**
@ -160,19 +162,18 @@ JS;
Requirements::customScript($js, 'MemberLoginFormFieldFocus'); Requirements::customScript($js, 'MemberLoginFormFieldFocus');
} }
/** public function restoreFormState()
* Get message from session
*/
protected function getMessageFromSession()
{ {
parent::restoreFormState();
$forceMessage = Session::get('MemberLoginForm.force_message'); $forceMessage = Session::get('MemberLoginForm.force_message');
if (($member = Member::currentUser()) && !$forceMessage) { if (($member = Member::currentUser()) && !$forceMessage) {
$this->message = _t( $message = _t(
'Member.LOGGEDINAS', 'Member.LOGGEDINAS',
"You're logged in as {name}.", "You're logged in as {name}.",
array('name' => $member->{$this->loggedInAsField}) array('name' => $member->{$this->loggedInAsField})
); );
$this->setMessage($message, ValidationResult::TYPE_INFO);
} }
// Reset forced message // Reset forced message
@ -180,7 +181,7 @@ JS;
Session::set('MemberLoginForm.force_message', false); Session::set('MemberLoginForm.force_message', false);
} }
return parent::getMessageFromSession(); return $this;
} }
@ -283,11 +284,8 @@ JS;
$member->logIn(); $member->logIn();
} }
Session::set( $message = _t('Member.WELCOMEBACK', "Welcome Back, {firstname}", array('firstname' => $firstname));
'Security.Message.message', Security::setLoginMessage($message, ValidationResult::TYPE_GOOD);
_t('Member.WELCOMEBACK', "Welcome Back, {firstname}", array('firstname' => $firstname))
);
Session::set("Security.Message.type", "good");
} }
return Controller::curr()->redirectBack(); return Controller::curr()->redirectBack();
} }

View File

@ -20,131 +20,131 @@ use SilverStripe\ORM\ValidationResult;
class PasswordValidator extends Object class PasswordValidator extends Object
{ {
private static $character_strength_tests = array( private static $character_strength_tests = array(
'lowercase' => '/[a-z]/', 'lowercase' => '/[a-z]/',
'uppercase' => '/[A-Z]/', 'uppercase' => '/[A-Z]/',
'digits' => '/[0-9]/', 'digits' => '/[0-9]/',
'punctuation' => '/[^A-Za-z0-9]/', 'punctuation' => '/[^A-Za-z0-9]/',
); );
protected $minLength, $minScore, $testNames, $historicalPasswordCount; protected $minLength, $minScore, $testNames, $historicalPasswordCount;
/** /**
* Minimum password length * Minimum password length
* *
* @param int $minLength * @param int $minLength
* @return $this * @return $this
*/ */
public function minLength($minLength) public function minLength($minLength)
{ {
$this->minLength = $minLength; $this->minLength = $minLength;
return $this; return $this;
} }
/** /**
* Check the character strength of the password. * Check the character strength of the password.
* *
* Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation")) * Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"))
* *
* @param int $minScore The minimum number of character tests that must pass * @param int $minScore The minimum number of character tests that must pass
* @param array $testNames The names of the tests to perform * @param array $testNames The names of the tests to perform
* @return $this * @return $this
*/ */
public function characterStrength($minScore, $testNames) public function characterStrength($minScore, $testNames)
{ {
$this->minScore = $minScore; $this->minScore = $minScore;
$this->testNames = $testNames; $this->testNames = $testNames;
return $this; return $this;
} }
/** /**
* Check a number of previous passwords that the user has used, and don't let them change to that. * Check a number of previous passwords that the user has used, and don't let them change to that.
* *
* @param int $count * @param int $count
* @return $this * @return $this
*/ */
public function checkHistoricalPasswords($count) public function checkHistoricalPasswords($count)
{ {
$this->historicalPasswordCount = $count; $this->historicalPasswordCount = $count;
return $this; return $this;
} }
/** /**
* @param String $password * @param String $password
* @param Member $member * @param Member $member
* @return ValidationResult * @return ValidationResult
*/ */
public function validate($password, $member) public function validate($password, $member)
{ {
$valid = ValidationResult::create(); $valid = ValidationResult::create();
if($this->minLength) { if ($this->minLength) {
if(strlen($password) < $this->minLength) { if (strlen($password) < $this->minLength) {
$valid->addError( $valid->addError(
sprintf( sprintf(
_t( _t(
'PasswordValidator.TOOSHORT', 'PasswordValidator.TOOSHORT',
'Password is too short, it must be %s or more characters long' 'Password is too short, it must be %s or more characters long'
), ),
$this->minLength $this->minLength
), ),
'bad', 'bad',
'TOO_SHORT' 'TOO_SHORT'
); );
} }
} }
if($this->minScore) { if ($this->minScore) {
$score = 0; $score = 0;
$missedTests = array(); $missedTests = array();
foreach($this->testNames as $name) { foreach ($this->testNames as $name) {
if(preg_match(self::config()->character_strength_tests[$name], $password)) { if (preg_match(self::config()->character_strength_tests[$name], $password)) {
$score++; $score++;
} else { } else {
$missedTests[] = _t( $missedTests[] = _t(
'PasswordValidator.STRENGTHTEST' . strtoupper($name), 'PasswordValidator.STRENGTHTEST' . strtoupper($name),
$name, $name,
'The user needs to add this to their password for more complexity' 'The user needs to add this to their password for more complexity'
); );
} }
} }
if($score < $this->minScore) { if ($score < $this->minScore) {
$valid->addError( $valid->addError(
sprintf( sprintf(
_t( _t(
'PasswordValidator.LOWCHARSTRENGTH', 'PasswordValidator.LOWCHARSTRENGTH',
'Please increase password strength by adding some of the following characters: %s' 'Please increase password strength by adding some of the following characters: %s'
), ),
implode(', ', $missedTests) implode(', ', $missedTests)
), ),
'bad', 'bad',
'LOW_CHARACTER_STRENGTH' 'LOW_CHARACTER_STRENGTH'
); );
} }
} }
if($this->historicalPasswordCount) { if ($this->historicalPasswordCount) {
$previousPasswords = MemberPassword::get() $previousPasswords = MemberPassword::get()
->where(array('"MemberPassword"."MemberID"' => $member->ID)) ->where(array('"MemberPassword"."MemberID"' => $member->ID))
->sort('"Created" DESC, "ID" DESC') ->sort('"Created" DESC, "ID" DESC')
->limit($this->historicalPasswordCount); ->limit($this->historicalPasswordCount);
/** @var MemberPassword $previousPassword */ /** @var MemberPassword $previousPassword */
foreach($previousPasswords as $previousPassword) { foreach ($previousPasswords as $previousPassword) {
if($previousPassword->checkPassword($password)) { if ($previousPassword->checkPassword($password)) {
$valid->addError( $valid->addError(
_t( _t(
'PasswordValidator.PREVPASSWORD', 'PasswordValidator.PREVPASSWORD',
'You\'ve already used that password in the past, please choose a new password' 'You\'ve already used that password in the past, please choose a new password'
), ),
'bad', 'bad',
'PREVIOUS_PASSWORD' 'PREVIOUS_PASSWORD'
); );
break; break;
} }
} }
} }
return $valid; return $valid;
} }
} }

View File

@ -13,50 +13,50 @@ use SilverStripe\ORM\DataObject;
*/ */
class PermissionRoleCode extends DataObject class PermissionRoleCode extends DataObject
{ {
private static $db = array( private static $db = array(
"Code" => "Varchar", "Code" => "Varchar",
); );
private static $has_one = array( private static $has_one = array(
"Role" => "SilverStripe\\Security\\PermissionRole", "Role" => "SilverStripe\\Security\\PermissionRole",
); );
private static $table_name = "PermissionRoleCode"; private static $table_name = "PermissionRoleCode";
public function validate() public function validate()
{ {
$result = parent::validate(); $result = parent::validate();
// Check that new code doesn't increase privileges, unless an admin is editing. // Check that new code doesn't increase privileges, unless an admin is editing.
$privilegedCodes = Permission::config()->privileged_permissions; $privilegedCodes = Permission::config()->privileged_permissions;
if ($this->Code if ($this->Code
&& in_array($this->Code, $privilegedCodes) && in_array($this->Code, $privilegedCodes)
&& !Permission::check('ADMIN') && !Permission::check('ADMIN')
) { ) {
$result->addError(sprintf( $result->addError(sprintf(
_t( _t(
'PermissionRoleCode.PermsError', 'PermissionRoleCode.PermsError',
'Can\'t assign code "%s" with privileged permissions (requires ADMIN access)' 'Can\'t assign code "%s" with privileged permissions (requires ADMIN access)'
), ),
$this->Code $this->Code
)); ));
} }
return $result; return $result;
} }
public function canCreate($member = null, $context = array()) public function canCreate($member = null, $context = array())
{ {
return Permission::check('APPLY_ROLES', 'any', $member); return Permission::check('APPLY_ROLES', 'any', $member);
} }
public function canEdit($member = null) public function canEdit($member = null)
{ {
return Permission::check('APPLY_ROLES', 'any', $member); return Permission::check('APPLY_ROLES', 'any', $member);
} }
public function canDelete($member = null) public function canDelete($member = null)
{ {
return Permission::check('APPLY_ROLES', 'any', $member); return Permission::check('APPLY_ROLES', 'any', $member);
} }
} }

View File

@ -21,6 +21,7 @@ use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
@ -324,7 +325,7 @@ class Security extends Controller implements TemplateGlobalProvider
// Somewhat hackish way to render a login form with an error message. // Somewhat hackish way to render a login form with an error message.
$me = new Security(); $me = new Security();
$form = $me->LoginForm(); $form = $me->LoginForm();
$form->sessionMessage($message, 'warning'); $form->sessionMessage($message, ValidationResult::TYPE_WARNING);
Session::set('MemberLoginForm.force_message', 1); Session::set('MemberLoginForm.force_message', 1);
$loginResponse = $me->login(); $loginResponse = $me->login();
if ($loginResponse instanceof HTTPResponse) { if ($loginResponse instanceof HTTPResponse) {
@ -340,8 +341,7 @@ class Security extends Controller implements TemplateGlobalProvider
$message = $messageSet['default']; $message = $messageSet['default'];
} }
Session::set("Security.Message.message", $message); static::setLoginMessage($message, ValidationResult::TYPE_WARNING);
Session::set("Security.Message.type", 'warning');
Session::set("BackURL", $_SERVER['REQUEST_URI']); Session::set("BackURL", $_SERVER['REQUEST_URI']);
@ -349,10 +349,10 @@ class Security extends Controller implements TemplateGlobalProvider
// Audit logging hook // Audit logging hook
$controller->extend('permissionDenied', $member); $controller->extend('permissionDenied', $member);
return $controller->redirect( return $controller->redirect(Controller::join_links(
Config::inst()->get('SilverStripe\\Security\\Security', 'login_url') static::config()->get('login_url'),
. "?BackURL=" . urlencode($_SERVER['REQUEST_URI']) "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
); ));
} }
protected function init() protected function init()
@ -559,11 +559,36 @@ class Security extends Controller implements TemplateGlobalProvider
} }
$messageType = Session::get('Security.Message.type'); $messageType = Session::get('Security.Message.type');
if ($messageType === 'bad') { $messageCast = Session::get('Security.Message.cast');
return "<p class=\"message $messageType\">$message</p>"; if ($messageCast !== ValidationResult::CAST_HTML) {
} else { $message = Convert::raw2xml($message);
return "<p>$message</p>";
} }
return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
}
/**
* Set the next message to display for the security login page. Defaults to warning
*
* @param string $message Message
* @param string $messageType Message type. One of ValidationResult::TYPE_*
* @param string $messageCast Message cast. One of ValidationResult::CAST_*
*/
public static function setLoginMessage(
$message,
$messageType = ValidationResult::TYPE_WARNING,
$messageCast = ValidationResult::CAST_TEXT
) {
Session::set("Security.Message.message", $message);
Session::set("Security.Message.type", $messageType);
Session::set("Security.Message.cast", $messageCast);
}
/**
* Clear login message
*/
public static function clearLoginMessage()
{
Session::clear("Security.Message");
} }
@ -603,7 +628,7 @@ class Security extends Controller implements TemplateGlobalProvider
$message = $this->getLoginMessage($messageType); $message = $this->getLoginMessage($messageType);
// We've displayed the message in the form output, so reset it for the next run. // We've displayed the message in the form output, so reset it for the next run.
Session::clear('Security.Message'); static::clearLoginMessage();
// only display tabs when more than one authenticator is provided // only display tabs when more than one authenticator is provided
// to save bandwidth and reduce the amount of custom styling needed // to save bandwidth and reduce the amount of custom styling needed

View File

@ -6,7 +6,7 @@ Feature: Log in
Scenario: Bad login Scenario: Bad login
Given I log in with "bad@example.com" and "badpassword" Given I log in with "bad@example.com" and "badpassword"
Then I will see a "bad" log-in message Then I will see a "error" log-in message
Scenario: Valid login Scenario: Valid login
Given I am logged in with "ADMIN" permissions Given I am logged in with "ADMIN" permissions

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Assets\Tests; namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\Image;
use SilverStripe\Assets\Storage\AssetStore; use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Tests\FileTest\MyCustomFile; use SilverStripe\Assets\Tests\FileTest\MyCustomFile;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
@ -178,22 +179,21 @@ class FileTest extends SapphireTest {
// Invalid ext // Invalid ext
$file->Name = 'asdf.php'; $file->Name = 'asdf.php';
$v = $file->validate(); $result = $file->validate();
$this->assertFalse($v->valid()); $this->assertFalse($result->isValid());
$this->assertContains('Extension is not allowed', $v->message()); $messages = $result->getMessages();
$this->assertEquals(1, count($messages));
$this->assertEquals('Extension is not allowed', $messages[0]['message']);
// Valid ext // Valid ext
$file->Name = 'asdf.txt'; $file->Name = 'asdf.txt';
$v = $file->validate(); $result = $file->validate();
$this->assertTrue($v->valid()); $this->assertTrue($result->isValid());
// Capital extension is valid as well // Capital extension is valid as well
$file->Name = 'asdf.TXT'; $file->Name = 'asdf.TXT';
$v = $file->validate(); $result = $file->validate();
$this->assertTrue($v->valid()); $this->assertTrue($result->isValid());
Config::inst()->remove(File::class, 'allowed_extensions');
Config::inst()->update(File::class, 'allowed_extensions', $orig);
} }
public function testAppCategory() { public function testAppCategory() {
@ -372,7 +372,7 @@ class FileTest extends SapphireTest {
public function testNameAndTitleGeneration() { public function testNameAndTitleGeneration() {
// When name is assigned, title is automatically assigned // When name is assigned, title is automatically assigned
$file = $this->objFromFixture('SilverStripe\\Assets\\Image', 'setfromname'); $file = $this->objFromFixture(Image::class, 'setfromname');
$this->assertEquals('FileTest', $file->Title); $this->assertEquals('FileTest', $file->Title);
} }
@ -386,13 +386,13 @@ class FileTest extends SapphireTest {
} }
public function testFileType() { public function testFileType() {
$file = $this->objFromFixture('SilverStripe\\Assets\\Image', 'gif'); $file = $this->objFromFixture(Image::class, 'gif');
$this->assertEquals("GIF image - good for diagrams", $file->FileType); $this->assertEquals("GIF image - good for diagrams", $file->FileType);
$file = $this->objFromFixture(File::class, 'pdf'); $file = $this->objFromFixture(File::class, 'pdf');
$this->assertEquals("Adobe Acrobat PDF file", $file->FileType); $this->assertEquals("Adobe Acrobat PDF file", $file->FileType);
$file = $this->objFromFixture('SilverStripe\\Assets\\Image', 'gifupper'); $file = $this->objFromFixture(Image::class, 'gifupper');
$this->assertEquals("GIF image - good for diagrams", $file->FileType); $this->assertEquals("GIF image - good for diagrams", $file->FileType);
/* Only a few file types are given special descriptions; the rest are unknown */ /* Only a few file types are given special descriptions; the rest are unknown */
@ -450,7 +450,7 @@ class FileTest extends SapphireTest {
$newTitle = "FileTest-folder-renamed"; $newTitle = "FileTest-folder-renamed";
//rename a folder's title //rename a folder's title
$folderID = $this->objFromFixture("SilverStripe\\Assets\\Folder","folder2")->ID; $folderID = $this->objFromFixture(Folder::class,"folder2")->ID;
$folder = DataObject::get_by_id(Folder::class,$folderID); $folder = DataObject::get_by_id(Folder::class,$folderID);
$folder->Title = $newTitle; $folder->Title = $newTitle;
$folder->write(); $folder->write();
@ -508,30 +508,30 @@ class FileTest extends SapphireTest {
} }
public function testCanEdit() { public function testCanEdit() {
$file = $this->objFromFixture('SilverStripe\\Assets\\Image', 'gif'); $file = $this->objFromFixture(Image::class, 'gif');
// Test anonymous permissions // Test anonymous permissions
Session::set('loggedInAs', null); Session::set('loggedInAs', null);
$this->assertFalse($file->canEdit(), "Anonymous users can't edit files"); $this->assertFalse($file->canEdit(), "Anonymous users can't edit files");
// Test permissionless user // Test permissionless user
$this->objFromFixture('SilverStripe\\Security\\Member', 'frontend')->logIn(); $this->objFromFixture(Member::class, 'frontend')->logIn();
$this->assertFalse($file->canEdit(), "Permissionless users can't edit files"); $this->assertFalse($file->canEdit(), "Permissionless users can't edit files");
// Test global CMS section users // Test global CMS section users
$this->objFromFixture('SilverStripe\\Security\\Member', 'cms')->logIn(); $this->objFromFixture(Member::class, 'cms')->logIn();
$this->assertTrue($file->canEdit(), "Users with all CMS section access can edit files"); $this->assertTrue($file->canEdit(), "Users with all CMS section access can edit files");
// Test cms access users without file access // Test cms access users without file access
$this->objFromFixture('SilverStripe\\Security\\Member', 'security')->logIn(); $this->objFromFixture(Member::class, 'security')->logIn();
$this->assertFalse($file->canEdit(), "Security CMS users can't edit files"); $this->assertFalse($file->canEdit(), "Security CMS users can't edit files");
// Test asset-admin user // Test asset-admin user
$this->objFromFixture('SilverStripe\\Security\\Member', 'assetadmin')->logIn(); $this->objFromFixture(Member::class, 'assetadmin')->logIn();
$this->assertTrue($file->canEdit(), "Asset admin users can edit files"); $this->assertTrue($file->canEdit(), "Asset admin users can edit files");
// Test admin // Test admin
$this->objFromFixture('SilverStripe\\Security\\Member', 'admin')->logIn(); $this->objFromFixture(Member::class, 'admin')->logIn();
$this->assertTrue($file->canEdit(), "Admins can edit files"); $this->assertTrue($file->canEdit(), "Admins can edit files");
} }

View File

@ -35,10 +35,7 @@ class Validator extends Upload_Validator implements TestOnly
// extension validation // extension validation
if(!$this->isValidExtension()) { if(!$this->isValidExtension()) {
$this->errors[] = _t( $this->errors[] = _t('File.INVALIDEXTENSIONSHORT', 'Extension is not allowed');
'File.INVALIDEXTENSION_SHORT',
'Extension is not allowed'
);
return false; return false;
} }

View File

@ -346,7 +346,7 @@ class AssetFieldTest extends FunctionalTest {
$form = new TestForm(); $form = new TestForm();
$form->loadDataFrom($data, true); $form->loadDataFrom($data, true);
if($form->validate()) { if($form->validationResult()->isValid()) {
$record = $form->getRecord(); $record = $form->getRecord();
$form->saveInto($record); $form->saveInto($record);
$record->write(); $record->write();

View File

@ -4,11 +4,13 @@ namespace SilverStripe\Forms\Tests\EmailFieldTest;
use Exception; use Exception;
use SilverStripe\Forms\Validator; use SilverStripe\Forms\Validator;
use SilverStripe\ORM\ValidationResult;
class TestValidator extends Validator class TestValidator extends Validator
{ {
public function validationError($fieldName, $message, $messageType = '') public function validationError(
{ $fieldName, $message, $messageType = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT
) {
throw new Exception($message); throw new Exception($message);
} }

View File

@ -18,11 +18,11 @@ class FieldGroupTest extends SapphireTest {
) )
); );
$textField->setError('Test error message', 'warning'); $textField->setMessage('Test error message', 'error');
$emailField->setError('Test error message', 'error'); $emailField->setMessage('Test error warning', 'warning');
$this->assertEquals('Test error message, Test error message.', $fieldGroup->Message()); $this->assertEquals('Test error message, Test error warning.', $fieldGroup->getMessage());
$this->assertEquals('warning. error', $fieldGroup->MessageType()); $this->assertEquals('error', $fieldGroup->getMessageType());
} }
} }

View File

@ -33,9 +33,7 @@ class FileFieldTest extends FunctionalTest {
); );
$fileField->setValue($fileFieldValue); $fileField->setValue($fileFieldValue);
$this->assertTrue( $this->assertTrue($form->validationResult()->isValid());
$form->validate()
);
} }
/** /**
@ -63,7 +61,7 @@ class FileFieldTest extends FunctionalTest {
$fileField->setValue($fileFieldValue); $fileField->setValue($fileFieldValue);
$this->assertFalse( $this->assertFalse(
$form->validate(), $form->validationResult()->isValid(),
'An error occured when uploading a file, but the validator returned true' 'An error occured when uploading a file, but the validator returned true'
); );
@ -72,7 +70,7 @@ class FileFieldTest extends FunctionalTest {
$fileField->setValue($fileFieldValue); $fileField->setValue($fileFieldValue);
$this->assertFalse( $this->assertFalse(
$form->validate(), $form->validationResult()->isValid(),
'An empty array was passed as parameter for an uploaded file, but the validator returned true' 'An empty array was passed as parameter for an uploaded file, but the validator returned true'
); );
@ -81,7 +79,7 @@ class FileFieldTest extends FunctionalTest {
$fileField->setValue($fileFieldValue); $fileField->setValue($fileFieldValue);
$this->assertFalse( $this->assertFalse(
$form->validate(), $form->validationResult()->isValid(),
'A null value was passed as parameter for an uploaded file, but the validator returned true' 'A null value was passed as parameter for an uploaded file, but the validator returned true'
); );
} }

View File

@ -332,11 +332,10 @@ class FormFieldTest extends SapphireTest {
$field = new FormField('MyField', 'My Field'); $field = new FormField('MyField', 'My Field');
$validator = new RequiredFields('MyField'); $validator = new RequiredFields('MyField');
$form = new Form(new Controller(), 'TestForm', new FieldList($field), new FieldList(), $validator); $form = new Form(new Controller(), 'TestForm', new FieldList($field), new FieldList(), $validator);
$form->validate(); $form->validationResult();
$form->setupFormErrors();
$schema = $field->getSchemaState(); $schema = $field->getSchemaState();
$this->assertEquals( $this->assertEquals(
['html' => '&quot;My Field&quot; is required'], '"My Field" is required',
$schema['message']['value'] $schema['message']['value']
); );
} }

View File

@ -77,7 +77,6 @@ class FormSchemaTest extends SapphireTest {
'name' => 'SecurityID', 'name' => 'SecurityID',
] ]
], ],
'valid' => null,
'messages' => [], 'messages' => [],
]; ];
@ -104,10 +103,9 @@ class FormSchemaTest extends SapphireTest {
] ]
], ],
'messages' => [[ 'messages' => [[
'value' => ['html' => 'All saved'], 'value' => 'All saved',
'type' => 'good' 'type' => 'good'
]], ]],
'valid' => null,
]; ];
$state = $formSchema->getState($form); $state = $formSchema->getState($form);
@ -123,7 +121,7 @@ class FormSchemaTest extends SapphireTest {
$form->loadDataFrom([ $form->loadDataFrom([
'Title' => null, 'Title' => null,
]); ]);
$this->assertFalse($form->validate()); $this->assertFalse($form->validationResult()->isValid());
$formSchema = new FormSchema(); $formSchema = new FormSchema();
$expected = [ $expected = [
'id' => 'Form_TestForm', 'id' => 'Form_TestForm',
@ -132,7 +130,7 @@ class FormSchemaTest extends SapphireTest {
'id' => 'Form_TestForm_Title', 'id' => 'Form_TestForm_Title',
'value' => null, 'value' => null,
'message' => [ 'message' => [
'value' => ['html' => '&quot;Title&quot; is required'], 'value' => '"Title" is required',
'type' => 'required' 'type' => 'required'
], ],
'data' => [], 'data' => [],
@ -146,7 +144,6 @@ class FormSchemaTest extends SapphireTest {
'name' => 'SecurityID', 'name' => 'SecurityID',
] ]
], ],
'valid' => false,
'messages' => [] 'messages' => []
]; ];
@ -165,7 +162,7 @@ class FormSchemaTest extends SapphireTest {
->setIcon('save'), ->setIcon('save'),
(new FormAction("cancel", "Cancel")) (new FormAction("cancel", "Cancel"))
->setUseButtonTag(true), ->setUseButtonTag(true),
new PopoverField("More options", [ $pop = new PopoverField("More options", [
new FormAction("publish", "Publish record"), new FormAction("publish", "Publish record"),
new FormAction("archive", "Archive"), new FormAction("archive", "Archive"),
]) ])

View File

@ -7,7 +7,8 @@ use SilverStripe\Forms\Tests\FormTest\ControllerWithSecurityToken;
use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck; use SilverStripe\Forms\Tests\FormTest\ControllerWithStrictPostCheck;
use SilverStripe\Forms\Tests\FormTest\Player; use SilverStripe\Forms\Tests\FormTest\Player;
use SilverStripe\Forms\Tests\FormTest\Team; use SilverStripe\Forms\Tests\FormTest\Team;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\NullSecurityToken;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\RandomGenerator; use SilverStripe\Security\RandomGenerator;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
@ -255,7 +256,7 @@ class FormTest extends FunctionalTest {
$form->saveInto($object); $form->saveInto($object);
$playersIds = $object->Players()->getIDList(); $playersIds = $object->Players()->getIDList();
$this->assertTrue($form->validate()); $this->assertTrue($form->validationResult()->isValid());
$this->assertEquals( $this->assertEquals(
$playersIds, $playersIds,
array(), array(),
@ -420,7 +421,7 @@ class FormTest extends FunctionalTest {
public function testSessionSuccessMessage() { public function testSessionSuccessMessage() {
$this->get('FormTest_Controller'); $this->get('FormTest_Controller');
$response = $this->post( $this->post(
'FormTest_Controller/Form', 'FormTest_Controller/Form',
array( array(
'Email' => 'test@test.com', 'Email' => 'test@test.com',
@ -439,12 +440,12 @@ class FormTest extends FunctionalTest {
public function testValidationException() { public function testValidationException() {
$this->get('FormTest_Controller'); $this->get('FormTest_Controller');
$response = $this->post( $this->post(
'FormTest_Controller/Form', 'FormTest_Controller/Form',
array( array(
'Email' => 'test@test.com', 'Email' => 'test@test.com',
'SomeRequiredField' => 'test', 'SomeRequiredField' => 'test',
'action_triggerException' => 1, 'action_doTriggerException' => 1,
) )
); );
$this->assertPartialMatchBySelector( $this->assertPartialMatchBySelector(
@ -468,12 +469,12 @@ class FormTest extends FunctionalTest {
SecurityToken::enable(); SecurityToken::enable();
$form1 = $this->getStubForm(); $form1 = $this->getStubForm();
$this->assertInstanceOf('SilverStripe\\Security\\SecurityToken', $form1->getSecurityToken()); $this->assertInstanceOf(SecurityToken::class, $form1->getSecurityToken());
SecurityToken::disable(); SecurityToken::disable();
$form2 = $this->getStubForm(); $form2 = $this->getStubForm();
$this->assertInstanceOf('SilverStripe\\Security\\NullSecurityToken', $form2->getSecurityToken()); $this->assertInstanceOf(NullSecurityToken::class, $form2->getSecurityToken());
SecurityToken::enable(); SecurityToken::enable();
} }
@ -500,7 +501,7 @@ class FormTest extends FunctionalTest {
SecurityToken::enable(); SecurityToken::enable();
$expectedToken = SecurityToken::inst()->getValue(); $expectedToken = SecurityToken::inst()->getValue();
$response = $this->get('FormTest_ControllerWithSecurityToken'); $this->get('FormTest_ControllerWithSecurityToken');
// can't use submitForm() as it'll automatically insert SecurityID into the POST data // can't use submitForm() as it'll automatically insert SecurityID into the POST data
$response = $this->post( $response = $this->post(
'FormTest_ControllerWithSecurityToken/Form', 'FormTest_ControllerWithSecurityToken/Form',
@ -518,7 +519,7 @@ class FormTest extends FunctionalTest {
$this->assertNotEquals($invalidToken, $expectedToken); $this->assertNotEquals($invalidToken, $expectedToken);
// Test token with request // Test token with request
$response = $this->get('FormTest_ControllerWithSecurityToken'); $this->get('FormTest_ControllerWithSecurityToken');
$response = $this->post( $response = $this->post(
'FormTest_ControllerWithSecurityToken/Form', 'FormTest_ControllerWithSecurityToken/Form',
array( array(
@ -541,7 +542,7 @@ class FormTest extends FunctionalTest {
$attrs = $matched[0]->attributes(); $attrs = $matched[0]->attributes();
$this->assertEquals('test@test.com', (string)$attrs['value'], 'Submitted data is preserved'); $this->assertEquals('test@test.com', (string)$attrs['value'], 'Submitted data is preserved');
$response = $this->get('FormTest_ControllerWithSecurityToken'); $this->get('FormTest_ControllerWithSecurityToken');
$tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID'); $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
$this->assertEquals( $this->assertEquals(
1, 1,
@ -561,13 +562,13 @@ class FormTest extends FunctionalTest {
} }
public function testStrictFormMethodChecking() { public function testStrictFormMethodChecking() {
$response = $this->get('FormTest_ControllerWithStrictPostCheck'); $this->get('FormTest_ControllerWithStrictPostCheck');
$response = $this->get( $response = $this->get(
'FormTest_ControllerWithStrictPostCheck/Form/?Email=test@test.com&action_doSubmit=1' 'FormTest_ControllerWithStrictPostCheck/Form/?Email=test@test.com&action_doSubmit=1'
); );
$this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method'); $this->assertEquals(405, $response->getStatusCode(), 'Submission fails with wrong method');
$response = $this->get('FormTest_ControllerWithStrictPostCheck'); $this->get('FormTest_ControllerWithStrictPostCheck');
$response = $this->post( $response = $this->post(
'FormTest_ControllerWithStrictPostCheck/Form', 'FormTest_ControllerWithStrictPostCheck/Form',
array( array(
@ -784,9 +785,7 @@ class FormTest extends FunctionalTest {
function testMessageEscapeHtml() { function testMessageEscapeHtml() {
$form = $this->getStubForm(); $form = $this->getStubForm();
$form->getController()->handleRequest(new HTTPRequest('GET', '/'), DataModel::inst()); // stub out request $form->setMessage('<em>Escaped HTML</em>', 'good', ValidationResult::CAST_TEXT);
$form->sessionMessage('<em>Escaped HTML</em>', 'good', true);
$form->setupFormErrors();
$parser = new CSSContentParser($form->forTemplate()); $parser = new CSSContentParser($form->forTemplate());
$messageEls = $parser->getBySelector('.message'); $messageEls = $parser->getBySelector('.message');
$this->assertContains( $this->assertContains(
@ -795,9 +794,7 @@ class FormTest extends FunctionalTest {
); );
$form = $this->getStubForm(); $form = $this->getStubForm();
$form->getController()->handleRequest(new HTTPRequest('GET', '/'), DataModel::inst()); // stub out request $form->setMessage('<em>Unescaped HTML</em>', 'good', ValidationResult::CAST_HTML);
$form->sessionMessage('<em>Unescaped HTML</em>', 'good', false);
$form->setupFormErrors();
$parser = new CSSContentParser($form->forTemplate()); $parser = new CSSContentParser($form->forTemplate());
$messageEls = $parser->getBySelector('.message'); $messageEls = $parser->getBySelector('.message');
$this->assertContains( $this->assertContains(
@ -806,11 +803,9 @@ class FormTest extends FunctionalTest {
); );
} }
function testFieldMessageEscapeHtml() { public function testFieldMessageEscapeHtml() {
$form = $this->getStubForm(); $form = $this->getStubForm();
$form->getController()->handleRequest(new HTTPRequest('GET', '/'), DataModel::inst()); // stub out request $form->Fields()->dataFieldByName('key1')->setMessage('<em>Escaped HTML</em>', 'good');
$form->getSessionValidationResult()->addFieldMessage('key1', '<em>Escaped HTML</em>', 'good');
$form->setupFormErrors();
$parser = new CSSContentParser($result = $form->forTemplate()); $parser = new CSSContentParser($result = $form->forTemplate());
$messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message'); $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
$this->assertContains( $this->assertContains(
@ -818,10 +813,12 @@ class FormTest extends FunctionalTest {
$messageEls[0]->asXML() $messageEls[0]->asXML()
); );
// Test with HTML
$form = $this->getStubForm(); $form = $this->getStubForm();
$form->getController()->handleRequest(new HTTPRequest('GET', '/'), DataModel::inst()); // stub out request $form
$form->getSessionValidationResult()->addFieldMessage('key1', '<em>Unescaped HTML</em>', 'good', null, false); ->Fields()
$form->setupFormErrors(); ->dataFieldByName('key1')
->setMessage('<em>Unescaped HTML</em>', 'good', ValidationResult::CAST_HTML);
$parser = new CSSContentParser($form->forTemplate()); $parser = new CSSContentParser($form->forTemplate());
$messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message'); $messageEls = $parser->getBySelector('#Form_Form_key1_Holder .message');
$this->assertContains( $this->assertContains(

View File

@ -4,9 +4,12 @@ namespace SilverStripe\Forms\Tests\FormTest;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/** /**
* @skipUpgrade * @skipUpgrade
*
* @method ManyManyList Players()
*/ */
class Team extends DataObject implements TestOnly class Team extends DataObject implements TestOnly
{ {

View File

@ -12,6 +12,8 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\NumericField; use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
/** /**
@ -54,6 +56,7 @@ class TestController extends Controller implements TestOnly
), ),
new FieldList( new FieldList(
FormAction::create('doSubmit'), FormAction::create('doSubmit'),
FormAction::create('doTriggerException'),
FormAction::create('doSubmitValidationExempt'), FormAction::create('doSubmitValidationExempt'),
FormAction::create('doSubmitActionExempt') FormAction::create('doSubmitActionExempt')
->setValidationExempt(true) ->setValidationExempt(true)
@ -75,6 +78,13 @@ class TestController extends Controller implements TestOnly
return $this->redirectBack(); return $this->redirectBack();
} }
public function doTriggerException($data, $form, $request) {
$result = new ValidationResult();
$result->addFieldError('Email', 'Error on Email field');
$result->addError('Error at top of form');
throw new ValidationException($result);
}
public function doSubmitValidationExempt($data, $form, $request) public function doSubmitValidationExempt($data, $form, $request)
{ {
$form->sessionMessage('Validation skipped', 'good'); $form->sessionMessage('Validation skipped', 'good');

View File

@ -54,7 +54,7 @@ class OptionsetFieldTest extends SapphireTest {
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
// ... but should not pass "RequiredFields" validation // ... but should not pass "RequiredFields" validation
$this->assertFalse($form->validate()); $this->assertFalse($form->validationResult()->isValid());
//disabled items shouldn't validate //disabled items shouldn't validate
$field->setDisabledItems(array('Five')); $field->setDisabledItems(array('Five'));

View File

@ -937,7 +937,7 @@ class UploadFieldTest extends FunctionalTest {
$form = new UploadFieldTest\UploadFieldTestForm(); $form = new UploadFieldTest\UploadFieldTestForm();
$form->loadDataFrom($data, true); $form->loadDataFrom($data, true);
if($form->validate()) { if($form->validationResult()->isValid()) {
$record = $form->getRecord(); $record = $form->getRecord();
$form->saveInto($record); $form->saveInto($record);
$record->write(); $record->write();

View File

@ -13,14 +13,10 @@ use SilverStripe\ORM\Connect\MySQLDatabase;
use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey; use SilverStripe\ORM\FieldType\DBPolymorphicForeignKey;
use SilverStripe\ORM\FieldType\DBVarchar; use SilverStripe\ORM\FieldType\DBVarchar;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Tests\ManyManyListTest\Category;
use SilverStripe\ORM\Tests\ManyManyListTest\ExtraFieldsObject;
use SilverStripe\ORM\Tests\ManyManyListTest\Product;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
use stdClass; use stdClass;
use ReflectionException; use ReflectionException;
use ReflectionMethod;
use InvalidArgumentException; use InvalidArgumentException;
class DataObjectTest extends SapphireTest { class DataObjectTest extends SapphireTest {
@ -1055,7 +1051,6 @@ class DataObjectTest extends SapphireTest {
public function testWritingInvalidDataObjectThrowsException() { public function testWritingInvalidDataObjectThrowsException() {
$validatedObject = new DataObjectTest\ValidatedObject(); $validatedObject = new DataObjectTest\ValidatedObject();
$this->setExpectedException(ValidationException::class); $this->setExpectedException(ValidationException::class);
$validatedObject->write(); $validatedObject->write();
} }
@ -1181,12 +1176,6 @@ class DataObjectTest extends SapphireTest {
); );
} }
protected function makeAccessible($object, $method) {
$reflectionMethod = new ReflectionMethod($object, $method);
$reflectionMethod->setAccessible(true);
return $reflectionMethod;
}
public function testValidateModelDefinitionsFailsWithArray() { public function testValidateModelDefinitionsFailsWithArray() {
Config::inst()->update(DataObjectTest\Team::class, 'has_one', array('NotValid' => array('NoArraysAllowed'))); Config::inst()->update(DataObjectTest\Team::class, 'has_one', array('NotValid' => array('NoArraysAllowed')));
$this->setExpectedException(InvalidArgumentException::class); $this->setExpectedException(InvalidArgumentException::class);

View File

@ -16,10 +16,10 @@ class ValidatedObject extends DataObject implements TestOnly
public function validate() public function validate()
{ {
if (!empty($this->Name)) { $result = ValidationResult::create();
return new ValidationResult(); if (empty($this->Name)) {
} else { $result->addError("This object needs a name. Otherwise it will have an identity crisis!");
return new ValidationResult(false, "This object needs a name. Otherwise it will have an identity crisis!");
} }
return $result;
} }
} }

View File

@ -31,17 +31,7 @@ class HierarchyTest extends SapphireTest {
$obj2aa = $this->objFromFixture(HierarchyTest\TestObject::class, 'obj2aa'); $obj2aa = $this->objFromFixture(HierarchyTest\TestObject::class, 'obj2aa');
$obj2->ParentID = $obj2aa->ID; $obj2->ParentID = $obj2aa->ID;
$obj2->write(); $obj2->write();
}
catch (ValidationException $e) {
$this->assertContains(
Convert::raw2xml('Infinite loop found within the "HierarchyTest_Object" hierarchy'),
$e->getMessage()
);
return;
}
$this->fail('Failed to prevent infinite loop in hierarchy.');
} }
/** /**
@ -194,7 +184,7 @@ class HierarchyTest extends SapphireTest {
} }
/** /**
* @covers SilverStripe\ORM\Hierarchy\Hierarchy::markChildren() * @covers \SilverStripe\ORM\Hierarchy\Hierarchy::markChildren()
*/ */
public function testMarkChildrenDoesntUnmarkPreviouslyMarked() { public function testMarkChildrenDoesntUnmarkPreviouslyMarked() {
$obj3 = $this->objFromFixture(HierarchyTest\TestObject::class, 'obj3'); $obj3 = $this->objFromFixture(HierarchyTest\TestObject::class, 'obj3');

View File

@ -12,7 +12,6 @@ class ValidationExceptionTest extends SapphireTest
* Test that ValidationResult object can correctly populate a ValidationException * Test that ValidationResult object can correctly populate a ValidationException
*/ */
public function testCreateFromValidationResult() { public function testCreateFromValidationResult() {
$result = new ValidationResult(); $result = new ValidationResult();
$result->addError('Not a valid result'); $result->addError('Not a valid result');
@ -20,8 +19,13 @@ class ValidationExceptionTest extends SapphireTest
$this->assertEquals(0, $exception->getCode()); $this->assertEquals(0, $exception->getCode());
$this->assertEquals('Not a valid result', $exception->getMessage()); $this->assertEquals('Not a valid result', $exception->getMessage());
$this->assertFalse($exception->getResult()->valid()); $this->assertFalse($exception->getResult()->isValid());
$this->assertEquals('Not a valid result', $exception->getResult()->message()); $this->assertContains([
'message' => 'Not a valid result',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
} }
@ -31,14 +35,26 @@ class ValidationExceptionTest extends SapphireTest
*/ */
public function testCreateFromComplexValidationResult() { public function testCreateFromComplexValidationResult() {
$result = new ValidationResult(); $result = new ValidationResult();
$result->addError('Invalid type') $result
->addError('Out of kiwis'); ->addError('Invalid type')
->addError('Out of kiwis');
$exception = new ValidationException($result); $exception = new ValidationException($result);
$this->assertEquals(0, $exception->getCode()); $this->assertEquals(0, $exception->getCode());
$this->assertEquals('Invalid type; Out of kiwis', $exception->getMessage()); $this->assertEquals('Invalid type', $exception->getMessage());
$this->assertEquals(false, $exception->getResult()->valid()); $this->assertEquals(false, $exception->getResult()->isValid());
$this->assertEquals('Invalid type; Out of kiwis', $exception->getResult()->message()); $this->assertContains([
'message' => 'Invalid type',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
$this->assertContains([
'message' => 'Out of kiwis',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
} }
/** /**
@ -50,26 +66,15 @@ class ValidationExceptionTest extends SapphireTest
$this->assertEquals(E_USER_ERROR, $exception->getCode()); $this->assertEquals(E_USER_ERROR, $exception->getCode());
$this->assertEquals('Error inferred from message', $exception->getMessage()); $this->assertEquals('Error inferred from message', $exception->getMessage());
$this->assertFalse($exception->getResult()->valid()); $this->assertFalse($exception->getResult()->isValid());
$this->assertEquals('Error inferred from message', $exception->getResult()->message()); $this->assertContains([
'message' => 'Error inferred from message',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
} }
/**
* Test that ValidationException can be created with both a ValidationResult
* and a custom message
*/
public function testCreateWithValidationResultAndMessage() {
$result = new ValidationResult();
$result->addError('Incorrect placement of cutlery');
$exception = new ValidationException($result, 'An error has occurred', E_USER_WARNING);
$this->assertEquals(E_USER_WARNING, $exception->getCode());
$this->assertEquals('An error has occurred', $exception->getMessage());
$this->assertFalse($exception->getResult()->valid());
$this->assertEquals('Incorrect placement of cutlery', $exception->getResult()->message());
}
/** /**
* Test that ValidationException can be created with both a ValidationResult * Test that ValidationException can be created with both a ValidationResult
* and a custom message * and a custom message
@ -78,13 +83,23 @@ class ValidationExceptionTest extends SapphireTest
$result = new ValidationResult(); $result = new ValidationResult();
$result->addError('A spork is not a knife') $result->addError('A spork is not a knife')
->addError('A knife is not a back scratcher'); ->addError('A knife is not a back scratcher');
$exception = new ValidationException($result, 'An error has occurred', E_USER_WARNING); $exception = new ValidationException($result, E_USER_WARNING);
$this->assertEquals(E_USER_WARNING, $exception->getCode()); $this->assertEquals(E_USER_WARNING, $exception->getCode());
$this->assertEquals('An error has occurred', $exception->getMessage()); $this->assertEquals('A spork is not a knife', $exception->getMessage());
$this->assertEquals(false, $exception->getResult()->valid()); $this->assertEquals(false, $exception->getResult()->isValid());
$this->assertEquals('A spork is not a knife; A knife is not a back scratcher', $this->assertContains([
$exception->getResult()->message()); 'message' => 'A spork is not a knife',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
$this->assertContains([
'message' => 'A knife is not a back scratcher',
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_ERROR,
'fieldName' => null,
], $exception->getResult()->getMessages());
} }
/** /**
@ -97,35 +112,30 @@ class ValidationExceptionTest extends SapphireTest
$anotherresult->addError("Eat with your mouth closed", 'bad', "EATING101"); $anotherresult->addError("Eat with your mouth closed", 'bad', "EATING101");
$yetanotherresult->addError("You didn't wash your hands", 'bad', "BECLEAN", false); $yetanotherresult->addError("You didn't wash your hands", 'bad', "BECLEAN", false);
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
$this->assertFalse($anotherresult->valid()); $this->assertFalse($anotherresult->isValid());
$this->assertFalse($yetanotherresult->valid()); $this->assertFalse($yetanotherresult->isValid());
$result->combineAnd($anotherresult) $result->combineAnd($anotherresult)
->combineAnd($yetanotherresult); ->combineAnd($yetanotherresult);
$this->assertFalse($result->valid()); $this->assertFalse($result->isValid());
$this->assertEquals(array( $this->assertEquals(
"EATING101" => "Eat with your mouth closed", [
"BECLEAN" => "You didn't wash your hands" 'EATING101' => [
), $result->messageList()); 'message' => 'Eat with your mouth closed',
} 'messageType' => 'bad',
'messageCast' => ValidationResult::CAST_TEXT,
/** 'fieldName' => null,
* Test that a ValidationException created with no contained ValidationResult ],
* will correctly populate itself with an inferred version 'BECLEAN' => [
*/ 'message' => 'You didn\'t wash your hands',
public function testCreateForField() { 'messageType' => 'bad',
$exception = ValidationException::create_for_field('Content', 'Content is required'); 'messageCast' => ValidationResult::CAST_HTML,
'fieldName' => null,
$this->assertEquals('Content is required', $exception->getMessage()); ],
$this->assertEquals(false, $exception->getResult()->valid()); ],
$result->getMessages()
$this->assertEquals(array( );
'Content' => array(
'message' => 'Content is required',
'messageType' => 'bad',
),
), $exception->getResult()->fieldErrors());
} }
/** /**
@ -137,23 +147,35 @@ class ValidationExceptionTest extends SapphireTest
$result->addMessage('A spork is not a knife', 'bad'); $result->addMessage('A spork is not a knife', 'bad');
$result->addError('A knife is not a back scratcher'); $result->addError('A knife is not a back scratcher');
$result->addFieldMessage('Title', 'Title is good', 'good'); $result->addFieldMessage('Title', 'Title is good', 'good');
$result->addFieldError('Content', 'Content is bad'); $result->addFieldError('Content', 'Content is bad', 'bad');
$this->assertEquals(array( $this->assertEquals([
'Title' => array( [
'fieldName' => null,
'message' => 'A spork is not a knife',
'messageType' => 'bad',
'messageCast' => ValidationResult::CAST_TEXT,
],
[
'fieldName' => null,
'message' => 'A knife is not a back scratcher',
'messageType' => 'error',
'messageCast' => ValidationResult::CAST_TEXT,
],
[
'fieldName' => 'Title',
'message' => 'Title is good', 'message' => 'Title is good',
'messageType' => 'good' 'messageType' => 'good',
), 'messageCast' => ValidationResult::CAST_TEXT,
'Content' => array( ],
[
'fieldName' => 'Content',
'message' => 'Content is bad', 'message' => 'Content is bad',
'messageType' => 'bad' 'messageType' => 'bad',
) 'messageCast' => ValidationResult::CAST_TEXT,
), $result->fieldErrors()); ]
], $result->getMessages());
$this->assertEquals('A spork is not a knife; A knife is not a back scratcher', $result->overallMessage());
$exception = ValidationException::create_for_field('Content', 'Content is required');
} }
} }

View File

@ -0,0 +1,35 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ValidationResult;
class ValidationResultTest extends SapphireTest
{
public function testSerialise() {
$result = new ValidationResult();
$result->addError("Error", ValidationResult::TYPE_ERROR, null, ValidationResult::CAST_HTML);
$result->addMessage("Message", ValidationResult::TYPE_GOOD);
$serialised = serialize($result);
/** @var ValidationResult $result2 */
$result2 = unserialize($serialised);
$this->assertEquals([
[
'message' => 'Error',
'fieldName' => null,
'messageCast' => ValidationResult::CAST_HTML,
'messageType' => ValidationResult::TYPE_ERROR,
],
[
'message' => 'Message',
'fieldName' => null,
'messageCast' => ValidationResult::CAST_TEXT,
'messageType' => ValidationResult::TYPE_GOOD,
]
], $result2->getMessages());
$this->assertFalse($result2->isValid());
}
}

View File

@ -2,12 +2,13 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Tests\GroupTest\TestMember; use SilverStripe\Security\Tests\GroupTest\TestMember;
use ReflectionMethod;
class GroupTest extends FunctionalTest { class GroupTest extends FunctionalTest {
@ -35,16 +36,17 @@ class GroupTest extends FunctionalTest {
$this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting'); $this->assertNull($g3->Code, 'Default title doesnt trigger attribute setting');
} }
/**
* @skipUpgrade
*/
public function testMemberGroupRelationForm() { public function testMemberGroupRelationForm() {
Session::set('loggedInAs', $this->idFromFixture(TestMember::class, 'admin')); Session::set('loggedInAs', $this->idFromFixture(TestMember::class, 'admin'));
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
$parentGroup = $this->objFromFixture(Group::class, 'parentgroup'); $parentGroup = $this->objFromFixture(Group::class, 'parentgroup');
$childGroup = $this->objFromFixture(Group::class, 'childgroup');
// Test single group relation through checkboxsetfield // Test single group relation through checkboxsetfield
/** @skipUpgrade */ $form = new GroupTest\MemberForm(new Controller(), 'Form');
$form = new GroupTest\MemberForm($this, 'Form');
$member = $this->objFromFixture(TestMember::class, 'admin'); $member = $this->objFromFixture(TestMember::class, 'admin');
$form->loadDataFrom($member); $form->loadDataFrom($member);
$checkboxSetField = $form->Fields()->fieldByName('Groups'); $checkboxSetField = $form->Fields()->fieldByName('Groups');
@ -75,9 +77,6 @@ class GroupTest extends FunctionalTest {
"Removing a previously added toplevel group works" "Removing a previously added toplevel group works"
); );
$this->assertContains($adminGroup->ID, $updatedGroups->column('ID')); $this->assertContains($adminGroup->ID, $updatedGroups->column('ID'));
// Test adding child group
} }
public function testUnsavedGroups() { public function testUnsavedGroups() {
@ -124,55 +123,47 @@ class GroupTest extends FunctionalTest {
$childGroupID = $this->idFromFixture(Group::class, 'childgroup'); $childGroupID = $this->idFromFixture(Group::class, 'childgroup');
$group->delete(); $group->delete();
$this->assertEquals(0, DataObject::get(Group::class, "\"ID\" = {$groupID}")->Count(), $this->assertEquals(0, DataObject::get(Group::class, "\"ID\" = {$groupID}")->count(),
'Group is removed'); 'Group is removed');
$this->assertEquals(0, DataObject::get('SilverStripe\\Security\\Permission', "\"GroupID\" = {$groupID}")->Count(), $this->assertEquals(0, DataObject::get(Permission::class, "\"GroupID\" = {$groupID}")->count(),
'Permissions removed along with the group'); 'Permissions removed along with the group');
$this->assertEquals(0, DataObject::get(Group::class, "\"ParentID\" = {$groupID}")->Count(), $this->assertEquals(0, DataObject::get(Group::class, "\"ParentID\" = {$groupID}")->count(),
'Child groups are removed'); 'Child groups are removed');
$this->assertEquals(0, DataObject::get(Group::class, "\"ParentID\" = {$childGroupID}")->Count(), $this->assertEquals(0, DataObject::get(Group::class, "\"ParentID\" = {$childGroupID}")->count(),
'Grandchild groups are removed'); 'Grandchild groups are removed');
} }
public function testValidatesPrivilegeLevelOfParent() { public function testValidatesPrivilegeLevelOfParent() {
$nonAdminUser = $this->objFromFixture(TestMember::class, 'childgroupuser');
$adminUser = $this->objFromFixture(TestMember::class, 'admin');
$nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup'); $nonAdminGroup = $this->objFromFixture(Group::class, 'childgroup');
$adminGroup = $this->objFromFixture(Group::class, 'admingroup'); $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
$nonAdminValidateMethod = new ReflectionMethod($nonAdminGroup, 'validate');
$nonAdminValidateMethod->setAccessible(true);
// Making admin group parent of a non-admin group, effectively expanding is privileges // Making admin group parent of a non-admin group, effectively expanding is privileges
$nonAdminGroup->ParentID = $adminGroup->ID; $nonAdminGroup->ParentID = $adminGroup->ID;
$this->logInWithPermission('APPLY_ROLES'); $this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminValidateMethod->invoke($nonAdminGroup); $result = $nonAdminGroup->validate();
$this->assertFalse( $this->assertFalse(
$result->valid(), $result->isValid(),
'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions' 'Members with only APPLY_ROLES can\'t assign parent groups with direct ADMIN permissions'
); );
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$result = $nonAdminValidateMethod->invoke($nonAdminGroup); $result = $nonAdminGroup->validate();
$this->assertTrue( $this->assertTrue(
$result->valid(), $result->isValid(),
'Members with ADMIN can assign parent groups with direct ADMIN permissions' 'Members with ADMIN can assign parent groups with direct ADMIN permissions'
); );
$nonAdminGroup->write(); $nonAdminGroup->write();
$newlyAdminGroup = $nonAdminGroup;
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1'); $inheritedAdminGroup = $this->objFromFixture(Group::class, 'group1');
$inheritedAdminMethod = new ReflectionMethod($inheritedAdminGroup, 'validate');
$inheritedAdminMethod->setAccessible(true);
$inheritedAdminGroup->ParentID = $adminGroup->ID; $inheritedAdminGroup->ParentID = $adminGroup->ID;
$inheritedAdminGroup->write(); // only works with ADMIN login $inheritedAdminGroup->write(); // only works with ADMIN login
$this->logInWithPermission('APPLY_ROLES'); $this->logInWithPermission('APPLY_ROLES');
$result = $inheritedAdminMethod->invoke($nonAdminGroup); $result = $nonAdminGroup->validate();
$this->assertFalse( $this->assertFalse(
$result->valid(), $result->isValid(),
'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission' 'Members with only APPLY_ROLES can\'t assign parent groups with inherited ADMIN permission'
); );
} }

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Security\Tests;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\PasswordEncryptor; use SilverStripe\Security\PasswordEncryptor;
use SilverStripe\Security\PasswordEncryptor_PHPHash; use SilverStripe\Security\PasswordEncryptor_PHPHash;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
@ -53,10 +54,11 @@ class MemberAuthenticatorTest extends SapphireTest {
); );
MemberAuthenticator::authenticate($data); MemberAuthenticator::authenticate($data);
$member = DataObject::get_by_id(Member::class, $member->ID); /** @var Member $member */
$member = DataObject::get_by_id(Member::class, $member->ID);
$this->assertEquals($member->PasswordEncryption, "sha1_v2.4"); $this->assertEquals($member->PasswordEncryption, "sha1_v2.4");
$result = $member->checkPassword('mypassword'); $result = $member->checkPassword('mypassword');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
} }
public function testNoLegacyPasswordHashMigrationOnIncompatibleAlgorithm() { public function testNoLegacyPasswordHashMigrationOnIncompatibleAlgorithm() {
@ -82,7 +84,7 @@ class MemberAuthenticatorTest extends SapphireTest {
$member = DataObject::get_by_id(Member::class, $member->ID); $member = DataObject::get_by_id(Member::class, $member->ID);
$this->assertEquals($member->PasswordEncryption, "crc32"); $this->assertEquals($member->PasswordEncryption, "crc32");
$result = $member->checkPassword('mypassword'); $result = $member->checkPassword('mypassword');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
} }
public function testCustomIdentifierField(){ public function testCustomIdentifierField(){
@ -139,10 +141,10 @@ class MemberAuthenticatorTest extends SapphireTest {
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'mypassword' 'Password' => 'mypassword'
), $form); ), $form);
$form->setupFormErrors(); $form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->ID, $member->ID); $this->assertEquals($result->ID, $member->ID);
$this->assertEmpty($form->Message()); $this->assertEmpty($form->getMessage());
// Test incorrect login // Test incorrect login
$form->clearMessage(); $form->clearMessage();
@ -150,10 +152,11 @@ class MemberAuthenticatorTest extends SapphireTest {
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), $form); ), $form);
$form->setupFormErrors(); $form->restoreFormState();
$this->assertEmpty($result); $this->assertEmpty($result);
$this->assertEquals(Convert::raw2xml(_t('Member.ERRORWRONGCRED')), $form->Message()); $this->assertEquals(_t('Member.ERRORWRONGCRED'), $form->getMessage());
$this->assertEquals('bad', $form->MessageType()); $this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
} }
/** /**
@ -170,10 +173,10 @@ class MemberAuthenticatorTest extends SapphireTest {
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'password' 'Password' => 'password'
), $form); ), $form);
$form->setupFormErrors(); $form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->Email, Security::default_admin_username()); $this->assertEquals($result->Email, Security::default_admin_username());
$this->assertEmpty($form->Message()); $this->assertEmpty($form->getMessage());
// Test incorrect login // Test incorrect login
$form->clearMessage(); $form->clearMessage();
@ -181,10 +184,14 @@ class MemberAuthenticatorTest extends SapphireTest {
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), $form); ), $form);
$form->setupFormErrors(); $form->restoreFormState();
$this->assertEmpty($result); $this->assertEmpty($result);
$this->assertEquals('The provided details don&#039;t seem to be correct. Please try again.', $form->Message()); $this->assertEquals(
$this->assertEquals('bad', $form->MessageType()); 'The provided details don\'t seem to be correct. Please try again.',
$form->getMessage()
);
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
} }
public function testDefaultAdminLockOut() public function testDefaultAdminLockOut()

View File

@ -87,6 +87,6 @@ class MemberCsvBulkLoaderTest extends SapphireTest {
// TODO Direct getter doesn't work, wtf! // TODO Direct getter doesn't work, wtf!
$this->assertEquals(Security::config()->password_encryption_algorithm, $member->getField('PasswordEncryption')); $this->assertEquals(Security::config()->password_encryption_algorithm, $member->getField('PasswordEncryption'));
$result = $member->checkPassword('mypassword'); $result = $member->checkPassword('mypassword');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
} }
} }

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Object; use SilverStripe\Core\Object;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Cookie;
@ -131,7 +132,7 @@ class MemberTest extends FunctionalTest {
'sha1_v2.4' 'sha1_v2.4'
); );
$result = $member->checkPassword('mynewpassword'); $result = $member->checkPassword('mynewpassword');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
Security::config()->password_encryption_algorithm = $origAlgo; Security::config()->password_encryption_algorithm = $origAlgo;
} }
@ -150,7 +151,7 @@ class MemberTest extends FunctionalTest {
'sha1_v2.4' 'sha1_v2.4'
); );
$result = $member->checkPassword(''); $result = $member->checkPassword('');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
} }
public function testSetPassword() { public function testSetPassword() {
@ -158,7 +159,7 @@ class MemberTest extends FunctionalTest {
$member->Password = "test1"; $member->Password = "test1";
$member->write(); $member->write();
$result = $member->checkPassword('test1'); $result = $member->checkPassword('test1');
$this->assertTrue($result->valid()); $this->assertTrue($result->isValid());
} }
/** /**
@ -212,7 +213,7 @@ class MemberTest extends FunctionalTest {
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$valid = $member->changePassword('32asDF##$$%%'); $valid = $member->changePassword('32asDF##$$%%');
$this->assertTrue($valid->valid()); $this->assertTrue($valid->isValid());
$this->assertEmailSent('testuser@example.com', null, 'Your password has been changed', $this->assertEmailSent('testuser@example.com', null, 'Your password has been changed',
'/testuser@example\.com/'); '/testuser@example\.com/');
@ -250,80 +251,81 @@ class MemberTest extends FunctionalTest {
* - at least 7 characters long * - at least 7 characters long
*/ */
public function testValidatePassword() { public function testValidatePassword() {
$member = $this->objFromFixture(Member::class, 'test'); /** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
Member::set_password_validator(new MemberTest\TestPasswordValidator()); Member::set_password_validator(new MemberTest\TestPasswordValidator());
// BAD PASSWORDS // BAD PASSWORDS
$valid = $member->changePassword('shorty'); $result = $member->changePassword('shorty');
$this->assertFalse($valid->valid()); $this->assertFalse($result->isValid());
$this->assertContains("TOO_SHORT", $valid->codeList()); $this->assertArrayHasKey("TOO_SHORT", $result->getMessages());
$valid = $member->changePassword('longone'); $result = $member->changePassword('longone');
$this->assertNotContains("TOO_SHORT", $valid->codeList()); $this->assertArrayNotHasKey("TOO_SHORT", $result->getMessages());
$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList()); $this->assertArrayHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
$this->assertFalse($valid->valid()); $this->assertFalse($result->isValid());
$valid = $member->changePassword('w1thNumb3rs'); $result = $member->changePassword('w1thNumb3rs');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList()); $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
// Clear out the MemberPassword table to ensure that the system functions properly in that situation // Clear out the MemberPassword table to ensure that the system functions properly in that situation
DB::query("DELETE FROM \"MemberPassword\""); DB::query("DELETE FROM \"MemberPassword\"");
// GOOD PASSWORDS // GOOD PASSWORDS
$valid = $member->changePassword('withSym###Ls'); $result = $member->changePassword('withSym###Ls');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList()); $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls2'); $result = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls3'); $result = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls4'); $result = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls5'); $result = $member->changePassword('withSym###Ls5');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls6'); $result = $member->changePassword('withSym###Ls6');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls7'); $result = $member->changePassword('withSym###Ls7');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
// CAN'T USE PASSWORDS 2-7, but I can use pasword 1 // CAN'T USE PASSWORDS 2-7, but I can use pasword 1
$valid = $member->changePassword('withSym###Ls2'); $result = $member->changePassword('withSym###Ls2');
$this->assertFalse($valid->valid()); $this->assertFalse($result->isValid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList()); $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
$valid = $member->changePassword('withSym###Ls5'); $result = $member->changePassword('withSym###Ls5');
$this->assertFalse($valid->valid()); $this->assertFalse($result->isValid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList()); $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
$valid = $member->changePassword('withSym###Ls7'); $result = $member->changePassword('withSym###Ls7');
$this->assertFalse($valid->valid()); $this->assertFalse($result->isValid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList()); $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
$valid = $member->changePassword('withSym###Ls'); $result = $member->changePassword('withSym###Ls');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
// HAVING DONE THAT, PASSWORD 2 is now available from the list // HAVING DONE THAT, PASSWORD 2 is now available from the list
$valid = $member->changePassword('withSym###Ls2'); $result = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls3'); $result = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
$valid = $member->changePassword('withSym###Ls4'); $result = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid()); $this->assertTrue($result->isValid());
Member::set_password_validator(null); Member::set_password_validator(null);
} }
@ -337,14 +339,14 @@ class MemberTest extends FunctionalTest {
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$valid = $member->changePassword("Xx?1234234"); $valid = $member->changePassword("Xx?1234234");
$this->assertTrue($valid->valid()); $this->assertTrue($valid->isValid());
$expiryDate = date('Y-m-d', time() + 90*86400); $expiryDate = date('Y-m-d', time() + 90*86400);
$this->assertEquals($expiryDate, $member->PasswordExpiry); $this->assertEquals($expiryDate, $member->PasswordExpiry);
Member::config()->password_expiry_days = null; Member::config()->password_expiry_days = null;
$valid = $member->changePassword("Xx?1234235"); $valid = $member->changePassword("Xx?1234235");
$this->assertTrue($valid->valid()); $this->assertTrue($valid->isValid());
$this->assertNull($member->PasswordExpiry); $this->assertNull($member->PasswordExpiry);
} }
@ -870,11 +872,11 @@ class MemberTest extends FunctionalTest {
'alc_device' => $firstHash->DeviceID 'alc_device' => $firstHash->DeviceID
) )
); );
$message = _t( $message = Convert::raw2xml(_t(
'Member.LOGGEDINAS', 'Member.LOGGEDINAS',
"You're logged in as {name}.", "You're logged in as {name}.",
array('name' => $m1->FirstName) array('name' => $m1->FirstName)
); ));
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->session()->inst_set('loggedInAs', null);
@ -924,9 +926,9 @@ class MemberTest extends FunctionalTest {
} }
public function testExpiredRememberMeHashAutologin() { public function testExpiredRememberMeHashAutologin() {
/** @var Member $m1 */
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
$m1->logIn(true);
$m1->login(true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
@ -936,7 +938,7 @@ class MemberTest extends FunctionalTest {
$firstHash->ExpiryDate = '2000-01-01 00:00:00'; $firstHash->ExpiryDate = '2000-01-01 00:00:00';
$firstHash->write(); $firstHash->write();
DBDateTime::set_mock_now('1999-12-31 23:59:59'); DBDatetime::set_mock_now('1999-12-31 23:59:59');
$response = $this->get( $response = $this->get(
'Security/login', 'Security/login',
@ -947,11 +949,11 @@ class MemberTest extends FunctionalTest {
'alc_device' => $firstHash->DeviceID 'alc_device' => $firstHash->DeviceID
) )
); );
$message = _t( $message = Convert::raw2xml(_t(
'Member.LOGGEDINAS', 'Member.LOGGEDINAS',
"You're logged in as {name}.", "You're logged in as {name}.",
array('name' => $m1->FirstName) array('name' => $m1->FirstName)
); ));
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->session()->inst_set('loggedInAs', null);
@ -1017,11 +1019,11 @@ class MemberTest extends FunctionalTest {
'alc_device' => $firstHash->DeviceID 'alc_device' => $firstHash->DeviceID
) )
); );
$message = _t( $message = Convert::raw2xml(_t(
'Member.LOGGEDINAS', 'Member.LOGGEDINAS',
"You're logged in as {name}.", "You're logged in as {name}.",
array('name' => $m1->FirstName) array('name' => $m1->FirstName)
); ));
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->session()->inst_set('loggedInAs', null);

View File

@ -11,10 +11,10 @@ class PasswordValidatorTest extends SapphireTest {
public function testValidate() { public function testValidate() {
$v = new PasswordValidator(); $v = new PasswordValidator();
$r = $v->validate('', new Member()); $r = $v->validate('', new Member());
$this->assertTrue($r->valid(), 'Empty password is valid by default'); $this->assertTrue($r->isValid(), 'Empty password is valid by default');
$r = $v->validate('mypassword', new Member()); $r = $v->validate('mypassword', new Member());
$this->assertTrue($r->valid(), 'Non-Empty password is valid by default'); $this->assertTrue($r->isValid(), 'Non-Empty password is valid by default');
} }
public function testValidateMinLength() { public function testValidateMinLength() {
@ -22,11 +22,11 @@ class PasswordValidatorTest extends SapphireTest {
$v->minLength(4); $v->minLength(4);
$r = $v->validate('123', new Member()); $r = $v->validate('123', new Member());
$this->assertFalse($r->valid(), 'Password too short'); $this->assertFalse($r->isValid(), 'Password too short');
$v->minLength(4); $v->minLength(4);
$r = $v->validate('1234', new Member()); $r = $v->validate('1234', new Member());
$this->assertTrue($r->valid(), 'Password long enough'); $this->assertTrue($r->isValid(), 'Password long enough');
} }
public function testValidateMinScore() { public function testValidateMinScore() {
@ -34,10 +34,10 @@ class PasswordValidatorTest extends SapphireTest {
$v->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation")); $v->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
$r = $v->validate('aA', new Member()); $r = $v->validate('aA', new Member());
$this->assertFalse($r->valid(), 'Passing too few tests'); $this->assertFalse($r->isValid(), 'Passing too few tests');
$r = $v->validate('aA1', new Member()); $r = $v->validate('aA1', new Member());
$this->assertTrue($r->valid(), 'Passing enough tests'); $this->assertTrue($r->isValid(), 'Passing enough tests');
} }
public function testHistoricalPasswordCount() { public function testHistoricalPasswordCount() {

View File

@ -6,7 +6,6 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\Security\PermissionRole; use SilverStripe\Security\PermissionRole;
use SilverStripe\Security\PermissionRoleCode; use SilverStripe\Security\PermissionRoleCode;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use ReflectionMethod;
class PermissionRoleTest extends FunctionalTest { class PermissionRoleTest extends FunctionalTest {
protected static $fixture_file = 'PermissionRoleTest.yml'; protected static $fixture_file = 'PermissionRoleTest.yml';
@ -24,31 +23,26 @@ class PermissionRoleTest extends FunctionalTest {
public function testValidatesPrivilegedPermissions() { public function testValidatesPrivilegedPermissions() {
$nonAdminCode = new PermissionRoleCode(array('Code' => 'CMS_ACCESS_CMSMain')); $nonAdminCode = new PermissionRoleCode(array('Code' => 'CMS_ACCESS_CMSMain'));
$nonAdminValidateMethod = new ReflectionMethod($nonAdminCode, 'validate');
$nonAdminValidateMethod->setAccessible(true);
$adminCode = new PermissionRoleCode(array('Code' => 'ADMIN')); $adminCode = new PermissionRoleCode(array('Code' => 'ADMIN'));
$adminValidateMethod = new ReflectionMethod($adminCode, 'validate');
$adminValidateMethod->setAccessible(true);
$this->logInWithPermission('APPLY_ROLES'); $this->logInWithPermission('APPLY_ROLES');
$result = $nonAdminValidateMethod->invoke($nonAdminCode); $result = $nonAdminCode->validate();
$this->assertTrue( $this->assertTrue(
$result->valid(), $result->isValid(),
'Members with only APPLY_ROLES can create non-privileged permission role codes' 'Members with only APPLY_ROLES can create non-privileged permission role codes'
); );
$this->logInWithPermission('APPLY_ROLES'); $this->logInWithPermission('APPLY_ROLES');
$result = $adminValidateMethod->invoke($adminCode); $result = $adminCode->validate();
$this->assertFalse( $this->assertFalse(
$result->valid(), $result->isValid(),
'Members with only APPLY_ROLES can\'t create privileged permission role codes' 'Members with only APPLY_ROLES can\'t create privileged permission role codes'
); );
$this->logInWithPermission('ADMIN'); $this->logInWithPermission('ADMIN');
$result = $adminValidateMethod->invoke($adminCode); $result = $adminCode->validate();
$this->assertTrue( $this->assertTrue(
$result->valid(), $result->isValid(),
'Members with ADMIN can create privileged permission role codes' 'Members with ADMIN can create privileged permission role codes'
); );
} }

View File

@ -6,6 +6,7 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBClassName; use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\LoginAttempt; use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
@ -444,7 +445,10 @@ class SecurityTest extends FunctionalTest {
$member->LockedOutUntil, $member->LockedOutUntil,
'User does not have a lockout time set if under threshold for failed attempts' 'User does not have a lockout time set if under threshold for failed attempts'
); );
$this->assertContains(Convert::raw2xml(_t('Member.ERRORWRONGCRED')), $this->loginErrorMessage()); $this->assertHasMessage(_t(
'Member.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.'
));
} else { } else {
// Fuzzy matching for time to avoid side effects from slow running tests // Fuzzy matching for time to avoid side effects from slow running tests
$this->assertGreaterThan( $this->assertGreaterThan(
@ -462,7 +466,7 @@ class SecurityTest extends FunctionalTest {
array('count' => Member::config()->lock_out_delay_mins) array('count' => Member::config()->lock_out_delay_mins)
); );
if($i > Member::config()->lock_out_after_incorrect_logins) { if($i > Member::config()->lock_out_after_incorrect_logins) {
$this->assertContains($msg, $this->loginErrorMessage()); $this->assertHasMessage($msg);
} }
} }
@ -491,9 +495,8 @@ class SecurityTest extends FunctionalTest {
$this->doTestLoginForm('testuser@example.com' , 'incorrectpassword'); $this->doTestLoginForm('testuser@example.com' , 'incorrectpassword');
} }
$this->assertNull($this->session()->inst_get('loggedInAs')); $this->assertNull($this->session()->inst_get('loggedInAs'));
$this->assertContains( $this->assertHasMessage(
$this->loginErrorMessage(), _t('Member.ERRORWRONGCRED','The provided details don\'t seem to be correct. Please try again.'),
Convert::raw2xml(_t('Member.ERRORWRONGCRED')),
'The user can retry with a wrong password after the lockout expires' 'The user can retry with a wrong password after the lockout expires'
); );
@ -560,9 +563,7 @@ class SecurityTest extends FunctionalTest {
$this->assertTrue(is_object($attempt)); $this->assertTrue(is_object($attempt));
$this->assertEquals($attempt->Status, 'Failure'); $this->assertEquals($attempt->Status, 'Failure');
$this->assertEquals($attempt->Email, 'wronguser@silverstripe.com'); $this->assertEquals($attempt->Email, 'wronguser@silverstripe.com');
$this->assertNotNull( $this->assertNotEmpty($this->getValidationResult()->getMessages(), 'An invalid email returns a message.');
$this->loginErrorMessage(), 'An invalid email returns a message.'
);
} }
public function testSuccessfulLoginAttempts() { public function testSuccessfulLoginAttempts() {
@ -640,12 +641,35 @@ class SecurityTest extends FunctionalTest {
); );
} }
/** /**
* Get the error message on the login form * Assert this message is in the current login form errors
*/ *
public function loginErrorMessage() { * @param string $expected
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result'); * @param string $errorMessage
return $result->message(); */
} protected function assertHasMessage($expected, $errorMessage = null) {
$messages = [];
$result = $this->getValidationResult();
if ($result) {
foreach($result->getMessages() as $message) {
$messages[] = $message['message'];
}
}
$this->assertContains($expected, $messages, $errorMessage);
}
/**
* Get validation result from last login form submission
*
* @return ValidationResult
*/
protected function getValidationResult() {
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result');
if ($result) {
/** @var ValidationResult $resultObj */
return unserialize($result);
}
return null;
}
} }