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); // Modify state.fields and replace state.messages
reduced = Object.assign({}, reduced, {
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),
}),
}); });
}
return Object.assign({}, // Can be safely discarded
state, delete reduced.errors;
this.props.stateOverrides, return deepFreeze(reduced);
{ fields }
);
} }
/** /**
@ -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,73 +351,44 @@ 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;
} }
/** /**
* Given a form, generate a response containing the requested form * Check if the current request has a X-Formschema-Request header set.
* schema if X-Formschema-Request header is set. * Used by conditional logic that responds to validation results
* *
* @param Form $form * @return bool
* @param String $id Optional, will default to the current request URL */
protected function getSchemaRequested() {
$parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
return !empty($parts);
}
/**
* Generate schema for the given form based on the X-Formschema-Request header value
*
* @param string $schemaID ID for this schema. Required.
* @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()
->getMultipartSchema($parts, $schemaID, $form, $errors);
if ($extraData) {
$data = array_merge($data, $extraData);
}
$response = new HTTPResponse(Convert::raw2json($data)); $response = new HTTPResponse(Convert::raw2json($data));
$response->addHeader('Content-Type', 'application/json'); $response->addHeader('Content-Type', 'application/json');
// Clear non-schema form validation / data / message
// since it does not need to be redirected
$form->clearMessage();
return $response; return $response;
} }
return null;
}
/**
* Returns a representation of the provided {@link Form} as structured data,
* based on the request data.
*
* @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;
}
/** /**
* Get link to schema url for a given form * Get link to schema url for a given form
@ -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

@ -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
@ -122,7 +121,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
@ -137,12 +136,12 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
{ {
// 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';
} }
@ -157,7 +156,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
*/ */
public function getBasename() public function getBasename()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return basename($this->getSourceURL()); return basename($this->getSourceURL());
@ -170,7 +169,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
*/ */
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);
@ -184,7 +183,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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
@ -198,7 +197,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
->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;
@ -211,7 +210,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
->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;
@ -224,7 +223,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
->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;
@ -232,7 +231,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getStream() public function getStream()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
@ -242,7 +241,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getString() public function getString()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
@ -252,7 +251,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
@ -282,7 +281,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
*/ */
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());
@ -290,7 +289,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getMetaData() public function getMetaData()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
@ -300,7 +299,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getMimeType() public function getMimeType()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return $this return $this
@ -310,7 +309,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getValue() public function getValue()
{ {
if(!$this->exists()) { if (!$this->exists()) {
return null; return null;
} }
return array( return array(
@ -322,7 +321,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function getVisibility() public function getVisibility()
{ {
if(empty($this->Filename)) { if (empty($this->Filename)) {
return null; return null;
} }
return $this return $this
@ -332,7 +331,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function exists() public function exists()
{ {
if(empty($this->Filename)) { if (empty($this->Filename)) {
return false; return false;
} }
return $this return $this
@ -363,7 +362,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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;
@ -399,7 +398,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
*/ */
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;
@ -431,13 +430,13 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
// 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;
} }
@ -455,7 +454,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
{ {
$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);
} }
} }
@ -470,25 +469,14 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
*/ */
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();
if(empty($extensions)) {
$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); $result->addError($message);
return false; return false;
} }
@ -496,7 +484,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
} }
@ -512,7 +500,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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;
@ -520,7 +508,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
public function deleteFile() public function deleteFile()
{ {
if(!$this->Filename) { if (!$this->Filename) {
return false; return false;
} }
@ -531,7 +519,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
@ -540,7 +528,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
@ -549,7 +537,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);
@ -558,7 +546,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail
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);

View File

@ -83,7 +83,7 @@ class FunctionalTest extends SapphireTest
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)));
} }
@ -91,12 +91,12 @@ class FunctionalTest extends SapphireTest
$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();
} }
@ -155,7 +155,7 @@ class FunctionalTest extends SapphireTest
{ {
$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;
@ -177,7 +177,7 @@ class FunctionalTest extends SapphireTest
{ {
$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;
@ -206,7 +206,7 @@ class FunctionalTest extends SapphireTest
{ {
$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;
@ -231,8 +231,8 @@ class FunctionalTest extends SapphireTest
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;
} }
} }
@ -261,10 +261,10 @@ class FunctionalTest extends SapphireTest
* *
* @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
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError * @throws PHPUnit_Framework_AssertionFailedError
* @return boolean
*/ */
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);
@ -273,19 +273,20 @@ class FunctionalTest extends SapphireTest
$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; if ($items) {
foreach ($items as $item) {
foreach($expectedMatches as $match) { $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true;
$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; $message = $message ?:
"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)) . "'";
foreach ($expectedMatches as $match) {
$this->assertTrue(isset($actuals[$match]), $message);
}
} }
/** /**
@ -297,10 +298,10 @@ class FunctionalTest extends SapphireTest
* *
* @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* matching tags as an array * @param array|string $expectedMatches The content of *all* matching tags as an array
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError * @throws PHPUnit_Framework_AssertionFailedError
* @return boolean
*/ */
public function assertExactMatchBySelector($selector, $expectedMatches) public function assertExactMatchBySelector($selector, $expectedMatches, $message = null)
{ {
if (is_string($expectedMatches)) { if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches); $expectedMatches = array($expectedMatches);
@ -311,18 +312,16 @@ class FunctionalTest extends SapphireTest
$actuals = array(); $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));
} }
} }
$this->assertTrue( $message = $message ?:
$expectedMatches == $actuals,
"Failed asserting the CSS selector '$selector' has an exact 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'", $actuals) . "'" . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
);
return true; $this->assertTrue($expectedMatches == $actuals, $message);
} }
/** /**
@ -334,10 +333,10 @@ class FunctionalTest extends SapphireTest
* *
* @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
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError * @throws PHPUnit_Framework_AssertionFailedError
* @return boolean
*/ */
public function assertPartialHTMLMatchBySelector($selector, $expectedMatches) public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null)
{ {
if (is_string($expectedMatches)) { if (is_string($expectedMatches)) {
$expectedMatches = array($expectedMatches); $expectedMatches = array($expectedMatches);
@ -346,23 +345,21 @@ class FunctionalTest extends SapphireTest
$items = $this->cssParser()->getBySelector($selector); $items = $this->cssParser()->getBySelector($selector);
$actuals = array(); $actuals = array();
if($items) { if ($items) {
/** @var SimpleXMLElement $item */ /** @var SimpleXMLElement $item */
foreach($items as $item) { foreach ($items as $item) {
$actuals[$item->asXML()] = true; $actuals[$item->asXML()] = true;
} }
} }
foreach($expectedMatches as $match) { $message = $message ?:
$this->assertTrue(
isset($actuals[$match]),
"Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" "Failed asserting the CSS selector '$selector' has a partial 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'", array_keys($actuals)) . "'";
);
}
return true; foreach ($expectedMatches as $match) {
$this->assertTrue(isset($actuals[$match]), $message);
}
} }
/** /**
@ -374,26 +371,27 @@ class FunctionalTest extends SapphireTest
* *
* @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 *all* matched tags as an array
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError * @throws PHPUnit_Framework_AssertionFailedError
*/ */
public function assertExactHTMLMatchBySelector($selector, $expectedMatches) public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null)
{ {
$items = $this->cssParser()->getBySelector($selector); $items = $this->cssParser()->getBySelector($selector);
$actuals = array(); $actuals = array();
if($items) { if ($items) {
/** @var SimpleXMLElement $item */ /** @var SimpleXMLElement $item */
foreach($items as $item) { foreach ($items as $item) {
$actuals[] = $item->asXML(); $actuals[] = $item->asXML();
} }
} }
$this->assertTrue( $message = $message ?:
$expectedMatches == $actuals,
"Failed asserting the CSS selector '$selector' has an exact 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'", $actuals) . "'" . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
);
$this->assertTrue($expectedMatches == $actuals, $message);
} }
/** /**
@ -423,7 +421,7 @@ class FunctionalTest extends SapphireTest
*/ */
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 {

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,8 +66,16 @@ use SilverStripe\View\SSViewer;
*/ */
class Form extends RequestHandler class Form extends RequestHandler
{ {
use FormMessage;
/**
* Form submission data is URL encoded
*/
const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded'; const ENC_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
/**
* Form submission data is multipart form
*/
const ENC_TYPE_MULTIPART = 'multipart/form-data'; const ENC_TYPE_MULTIPART = 'multipart/form-data';
/** /**
@ -164,16 +172,6 @@ class Form extends RequestHandler
*/ */
protected $buttonClickedFunc; protected $buttonClickedFunc;
/**
* @var string|null
*/
protected $message;
/**
* @var string|null
*/
protected $messageType;
/** /**
* Should we redirect the user back down to the * Should we redirect the user back down to the
* the form on validation errors rather then just the page * the form on validation errors rather then just the page
@ -228,8 +226,6 @@ class Form extends RequestHandler
private static $casting = array( private static $casting = array(
'AttributesHTML' => 'HTMLFragment', 'AttributesHTML' => 'HTMLFragment',
'FormAttributes' => 'HTMLFragment', 'FormAttributes' => 'HTMLFragment',
'MessageType' => 'Text',
'Message' => 'HTMLFragment',
'FormName' => 'Text', 'FormName' => 'Text',
'Legend' => 'HTMLFragment', 'Legend' => 'HTMLFragment',
); );
@ -285,11 +281,11 @@ class Form extends RequestHandler
$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 {
@ -312,36 +308,151 @@ class Form extends RequestHandler
); );
/** /**
* 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 setupFormErrors($result = null, $data = null) { public function restoreFormState()
if(!$result) $result = Session::get("FormInfo.{$this->FormName()}.result"); {
if(!$result) return; // Restore messages
$result = $this->getSessionValidationResult();
foreach($result->fieldErrors() as $fieldName => $fieldError) { if (isset($result)) {
$field = $this->fields->dataFieldByName($fieldName); $this->loadMessagesFrom($result);
$field->setError($fieldError['message'], $fieldError['messageType']);
} }
//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);
// load data in from previous submission upon error // load data in from previous submission upon error
if(!$data) $data = Session::get("FormInfo.{$this->FormName()}.data"); $data = $this->getSessionData();
if($data) $this->loadDataFrom($data); if (isset($data)) {
$this->loadDataFrom($data);
}
return $this;
} }
/** /**
* Save information to the session to be picked up by {@link setUpFormErrors()} * Flush persistant form state details
*/ */
public function saveFormErrorsToSession($result = null, $data = null) { public function clearFormState()
Session::set("FormInfo.{$this->FormName()}.result", $result); {
Session::clear("FormInfo.{$this->FormName()}.result");
Session::clear("FormInfo.{$this->FormName()}.data");
}
/**
* Return any form data stored in the session
*
* @return array
*/
public function getSessionData()
{
return Session::get("FormInfo.{$this->FormName()}.data");
}
/**
* Store the given form data in the session
*
* @param array $data
*/
public function setSessionData($data)
{
Session::set("FormInfo.{$this->FormName()}.data", $data); Session::set("FormInfo.{$this->FormName()}.data", $data);
} }
/**
* Return any ValidationResult instance stored for this object
*
* @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 * set up the default classes for the form. This is done on construct so that the default classes can be removed
* after instantiation * after instantiation
@ -363,14 +474,15 @@ class Form extends RequestHandler
* if the form is valid. * if the form is valid.
* *
* @param HTTPRequest $request * @param HTTPRequest $request
* @return HTTPResponse
* @throws HTTPResponse_Exception * @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"));
@ -389,8 +501,9 @@ class Form extends RequestHandler
$this->loadDataFrom($vars, true, $allowedFields); $this->loadDataFrom($vars, true, $allowedFields);
// Protection against CSRF attacks // Protection against CSRF attacks
// @todo Move this to SecurityTokenField::validate()
$token = $this->getSecurityToken(); $token = $this->getSecurityToken();
if( ! $token->checkRequest($request)) { if (! $token->checkRequest($request)) {
$securityID = $token->getName(); $securityID = $token->getName();
if (empty($vars[$securityID])) { if (empty($vars[$securityID])) {
$this->httpError(400, _t( $this->httpError(400, _t(
@ -400,24 +513,26 @@ class Form extends RequestHandler
)); ));
} else { } else {
// Clear invalid token on refresh // Clear invalid token on refresh
$this->clearFormState();
$data = $this->getData(); $data = $this->getData();
unset($data[$securityID]); unset($data[$securityID]);
Session::set("FormInfo.{$this->FormName()}.data", $data); $this->setSessionData($data);
Session::set("FormInfo.{$this->FormName()}.errors", array()); $this->sessionError(_t(
$this->sessionMessage( "Form.CSRF_EXPIRED_MESSAGE",
_t("Form.CSRF_EXPIRED_MESSAGE", "Your session has expired. Please re-submit the form."), "Your session has expired. Please re-submit the form."
"warning" ));
);
// Return the user
return $this->controller->redirectBack(); return $this->controller->redirectBack();
} }
} }
// Determine the action button clicked // Determine the action button clicked
$funcName = null; $funcName = null;
foreach($vars as $paramName => $paramVal) { foreach ($vars as $paramName => $paramVal) {
if(substr($paramName,0,7) == 'action_') { if (substr($paramName, 0, 7) == 'action_') {
// Break off querystring arguments included in the action // Break off querystring arguments included in the action
if(strpos($paramName,'?') !== false) { if (strpos($paramName, '?') !== false) {
list($paramName, $paramVars) = explode('?', $paramName, 2); list($paramName, $paramVars) = explode('?', $paramName, 2);
$newRequestParams = array(); $newRequestParams = array();
parse_str($paramVars, $newRequestParams); parse_str($paramVars, $newRequestParams);
@ -425,17 +540,17 @@ class Form extends RequestHandler
} }
// Cleanup action_, _x and _y from image fields // Cleanup action_, _x and _y from image fields
$funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName); $funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName);
break; break;
} }
} }
// If the action wasn't set, choose the default on the form. // If the action wasn't set, choose the default on the form.
if(!isset($funcName) && $defaultAction = $this->defaultAction()){ if (!isset($funcName) && $defaultAction = $this->defaultAction()) {
$funcName = $defaultAction->actionName(); $funcName = $defaultAction->actionName();
} }
if(isset($funcName)) { if (isset($funcName)) {
$this->setButtonClicked($funcName); $this->setButtonClicked($funcName);
} }
@ -462,59 +577,36 @@ class Form extends RequestHandler
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.
if ($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this, $request);
}
// Check for inline actions
if ($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
return $field->$funcName($vars, $this, $request);
}
} catch (ValidationException $e) {
// The ValdiationResult contains all the relevant metadata // The ValdiationResult contains all the relevant metadata
$result = $e->getResult(); $result = $e->getResult();
$this->loadMessagesFrom($result);
return $this->getValidationErrorResponse($result); return $this->getValidationErrorResponse($result);
} }
// First, try a handler method on the controller (has been checked for allowed_actions above already)
if($this->controller->hasMethod($funcName)) {
return $this->controller->$funcName($vars, $this, $request);
// Otherwise, try a handler method on the form object.
} elseif($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this, $request);
} elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) {
return $field->$funcName($vars, $this, $request);
}
return $this->httpError(404); return $this->httpError(404);
} }
@ -578,50 +670,76 @@ class Form extends RequestHandler
* and can be overruled by setting {@link $validationResponseCallback}. * and can be overruled by setting {@link $validationResponseCallback}.
* *
* @param ValidationResult $result * @param ValidationResult $result
* @return HTTPResponse|string * @return HTTPResponse
*/ */
protected function getValidationErrorResponse(ValidationResult $result) { protected function getValidationErrorResponse(ValidationResult $result)
{
// Check for custom handling mechanism
$callback = $this->getValidationResponseCallback(); $callback = $this->getValidationResponseCallback();
if($callback && $callbackResponse = $callback($result)) { if ($callback && $callbackResponse = call_user_func($callback, $result)) {
return $callbackResponse; return $callbackResponse;
} }
$request = $this->getRequest(); // Check if handling via ajax
if($request->isAjax()) { if ($this->getRequest()->isAjax()) {
// Special case for legacy Validator.js implementation return $this->getAjaxErrorResponse($result);
// (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; // Prior to redirection, persist this result in session to re-display on redirect
$this->setSessionValidationResult($result);
$this->setSessionData($this->getData());
} else { // Determine redirection method
// Save the relevant information in the session if ($this->getRedirectToFormOnValidationError() && ($pageURL = $this->getRedirectReferer())) {
$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->redirect($pageURL . '#' . $this->FormName());
} }
}
}
return $this->controller->redirectBack(); 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);
} }
/** /**
@ -633,10 +751,10 @@ class Form extends RequestHandler
*/ */
protected function checkFieldsForAction($fields, $funcName) protected function checkFieldsForAction($fields, $funcName)
{ {
foreach($fields as $field){ foreach ($fields as $field) {
/** @skipUpgrade */ /** @skipUpgrade */
if(method_exists($field, 'FieldList')) { if (method_exists($field, 'FieldList')) {
if($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) { if ($field = $this->checkFieldsForAction($field->FieldList(), $funcName)) {
return $field; return $field;
} }
} elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) { } elseif ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) {
@ -661,7 +779,7 @@ class Form extends RequestHandler
{ {
$field = $this->Fields()->dataFieldByName($request->param('FieldName')); $field = $this->Fields()->dataFieldByName($request->param('FieldName'));
if($field) { if ($field) {
return $field; return $field;
} else { } else {
// falling back to fieldByName, e.g. for getting tabs // falling back to fieldByName, e.g. for getting tabs
@ -702,31 +820,19 @@ class Form extends RequestHandler
return $this->redirectToFormOnValidationError; 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 * @param FormTransformation $trans
*/ */
public function transform(FormTransformation $trans) public function transform(FormTransformation $trans)
{ {
$newFields = new FieldList(); $newFields = new FieldList();
foreach($this->fields as $field) { foreach ($this->fields as $field) {
$newFields->push($field->transform($trans)); $newFields->push($field->transform($trans));
} }
$this->fields = $newFields; $this->fields = $newFields;
$newActions = new FieldList(); $newActions = new FieldList();
foreach($this->actions as $action) { foreach ($this->actions as $action) {
$newActions->push($action->transform($trans)); $newActions->push($action->transform($trans));
} }
$this->actions = $newActions; $this->actions = $newActions;
@ -754,7 +860,7 @@ class Form extends RequestHandler
*/ */
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);
} }
@ -844,7 +950,7 @@ class Form extends RequestHandler
*/ */
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);
} }
@ -934,7 +1040,7 @@ class Form extends RequestHandler
*/ */
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;
@ -954,7 +1060,7 @@ class Form extends RequestHandler
'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'] = '';
} }
@ -1014,7 +1120,7 @@ class Form extends RequestHandler
// 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) . "\"";
} }
@ -1045,8 +1151,8 @@ class Form extends RequestHandler
*/ */
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);
} }
@ -1116,7 +1222,7 @@ class Form extends RequestHandler
{ {
$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;
@ -1184,7 +1290,7 @@ class Form extends RequestHandler
*/ */
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';
@ -1244,7 +1350,7 @@ class Form extends RequestHandler
{ {
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);
@ -1358,113 +1464,36 @@ class Form extends RequestHandler
return new Form_FieldMap($this); return new Form_FieldMap($this);
} }
/**
* The next functions store and modify the forms
* message attributes. messages are stored in session under
* $_SESSION[formname][message];
*
* @return string
*/
public function Message() {
return $this->message;
}
/**
* @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->messageType = $type;
return $this;
}
/** /**
* Set a message to the session, for display next time this form is shown. * Set a message 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 sessionMessage($message, $type, $escapeHtml = true) public function sessionMessage($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
{ {
// Benign message $this->setMessage($message, $type, $cast);
if($type == "good") { $result = $this->getSessionValidationResult() ?: ValidationResult::create();
$this->getSessionValidationResult()->addMessage($message, $type, null, $escapeHtml); $result->addMessage($message, $type, null, $cast);
$this->setSessionValidationResult($result);
// Bad message causing a validation error
} else {
$this->getSessionValidationResult()->addError($message, $type, null, $escapeHtml
);
}
} }
/** /**
* @deprecated 3.1 * Set an error to the session, for display next time this form is shown.
*/
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 * @param string $message the text of the message
* @param string $type Should be set to good, bad, or warning.
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
* Bool values will be treated as plain text flag.
*/ */
public function getSessionValidationResult() { public function sessionError($message, $type = ValidationResult::TYPE_ERROR, $cast = ValidationResult::CAST_TEXT)
$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; $this->setMessage($message, $type, $cast);
Session::clear("FormInfo.{$this->FormName()}.result"); $result = $this->getSessionValidationResult() ?: ValidationResult::create();
Session::clear("FormInfo.{$this->FormName()}.data"); $result->addError($message, $type, null, $cast);
} $this->setSessionValidationResult($result);
public function resetValidation() {
Session::clear("FormInfo.{$this->FormName()}.data");
Session::clear("FormInfo.{$this->FormName()}.result");
} }
/** /**
@ -1504,48 +1533,25 @@ class Form extends RequestHandler
* 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(); {
// Valid
if($result->valid()) {
return true;
// Invalid
} else {
$this->saveFormErrorsToSession($result, $this->getData());
return false;
}
}
/**
* Experimental method - return a ValidationResult for the validator
* @return [type] [description]
*/
private function validationResult() {
// Start with a "valid" validation result
$result = ValidationResult::create();
// Opportunity to invalidate via validator // Opportunity to invalidate via validator
$action = $this->buttonClicked(); $action = $this->buttonClicked();
if($action && $this->actionIsValidationExempt($action)) { if ($action && $this->actionIsValidationExempt($action)) {
return ValidationResult::create();
}
// Invoke validator
if ($this->validator) {
$result = $this->validator->validate();
$this->loadMessagesFrom($result);
return $result; return $result;
} }
if($this->validator){ // Successful result
$errors = $this->validator->validate(); return ValidationResult::create();
// Convert the old-style Validator result into a ValidationResult
if($errors){
foreach($errors as $error) {
$result->addFieldError($error['fieldName'], $error['message'], $error['messageType']);
}
}
}
return $result;
} }
const MERGE_DEFAULT = 0; const MERGE_DEFAULT = 0;
@ -1595,11 +1601,11 @@ class Form extends RequestHandler
* *
* @param array $fieldList An optional list of fields to process. This can be useful when you have a * @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. * form that has some fields that save to one object, and some that save to another.
* @return Form * @return $this
*/ */
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;
} }
@ -1618,12 +1624,16 @@ class Form extends RequestHandler
// dont include fields without data // dont include fields without data
$dataFields = $this->Fields()->dataFields(); $dataFields = $this->Fields()->dataFields();
if ($dataFields) { if (!$dataFields) {
return $this;
}
/** @var FormField $field */
foreach ($dataFields as $field) { foreach ($dataFields as $field) {
$name = $field->getName(); $name = $field->getName();
// Skip fields that have been excluded // Skip fields that have been excluded
if($fieldList && !in_array($name, $fieldList)) { if ($fieldList && !in_array($name, $fieldList)) {
continue; continue;
} }
@ -1637,7 +1647,7 @@ class Form extends RequestHandler
// The value from $data for this field // The value from $data for this field
$val = null; $val = null;
if(is_object($data)) { if (is_object($data)) {
$exists = ( $exists = (
isset($data->$name) || isset($data->$name) ||
$data->hasMethod($name) || $data->hasMethod($name) ||
@ -1648,15 +1658,15 @@ class Form extends RequestHandler
$val = $data->__get($name); $val = $data->__get($name);
} }
} elseif (is_array($data)) { } elseif (is_array($data)) {
if(array_key_exists($name, $data)) { if (array_key_exists($name, $data)) {
$exists = true; $exists = true;
$val = $data[$name]; $val = $data[$name];
} // If field is in array-notation we need to access nested data } // If field is in array-notation we need to access nested data
else if(strpos($name,'[')) { elseif (strpos($name, '[')) {
// First encode data using PHP's method of converting nested arrays to form data // First encode data using PHP's method of converting nested arrays to form data
$flatData = urldecode(http_build_query($data)); $flatData = urldecode(http_build_query($data));
// Then pull the value out from that flattened string // Then pull the value out from that flattened string
preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches); preg_match('/' . addcslashes($name, '[]') . '=([^&]*)/', $flatData, $matches);
if (isset($matches[1])) { if (isset($matches[1])) {
$exists = true; $exists = true;
@ -1666,8 +1676,8 @@ class Form extends RequestHandler
} }
// save to the field if either a value is given, or loading of blank/undefined values is forced // save to the field if either a value is given, or loading of blank/undefined values is forced
if($exists){ if ($exists) {
if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){ 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 // pass original data as well so composite fields can act on the additional information
$field->setValue($val, $data); $field->setValue($val, $data);
} }
@ -1675,8 +1685,6 @@ class Form extends RequestHandler
$field->setValue($val, $data); $field->setValue($val, $data);
} }
} }
}
return $this; return $this;
} }
@ -1699,14 +1707,12 @@ class Form extends RequestHandler
continue; continue;
} }
$saveMethod = "save{$field->getName()}"; $saveMethod = "save{$field->getName()}";
if ($field->getName() == "ClassName") {
if($field->getName() == "ClassName"){
$lastField = $field; $lastField = $field;
}else if( $dataObject->hasMethod( $saveMethod ) ){ } elseif ($dataObject->hasMethod($saveMethod)) {
$dataObject->$saveMethod( $field->dataValue()); $dataObject->$saveMethod($field->dataValue());
} else if($field->getName() != "ID"){ } elseif ($field->getName() !== "ID") {
$field->saveInto($dataObject); $field->saveInto($dataObject);
} }
} }
@ -1731,9 +1737,9 @@ class Form extends RequestHandler
$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();
} }
} }
@ -1818,7 +1824,7 @@ class Form extends RequestHandler
"Actions" => "", "Actions" => "",
)); ));
if(is_string($template)) { if (is_string($template)) {
$template = new SSViewer($template); $template = new SSViewer($template);
} }
@ -1865,7 +1871,7 @@ class Form extends RequestHandler
$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;
}); });
@ -1880,7 +1886,7 @@ class Form extends RequestHandler
*/ */
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;
@ -1970,7 +1976,7 @@ class Form extends RequestHandler
{ {
//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;
} }
@ -1998,14 +2004,14 @@ class Form extends RequestHandler
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;

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

@ -52,7 +52,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/ */
public function augmentColumns($gridField, &$columns) public function augmentColumns($gridField, &$columns)
{ {
if(!in_array('Actions', $columns)) { if (!in_array('Actions', $columns)) {
$columns[] = 'Actions'; $columns[] = 'Actions';
} }
} }
@ -79,7 +79,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/ */
public function getColumnMetadata($gridField, $columnName) public function getColumnMetadata($gridField, $columnName)
{ {
if($columnName == 'Actions') { if ($columnName == 'Actions') {
return array('title' => ''); return array('title' => '');
} }
} }
@ -115,8 +115,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/ */
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;
} }
@ -130,7 +130,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
->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;
} }
@ -143,7 +143,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
) )
->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();
} }
@ -159,24 +159,26 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
*/ */
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,6 +18,7 @@ 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;
@ -399,11 +398,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
} }
// 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)
@ -417,7 +412,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
) )
); );
$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);
@ -486,52 +481,21 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
return $this->record; return $this->record;
} }
/**
* Generate a response object for a form validation error
*
* @param Form $form The source form
* @param ValidationException $e The validation error message
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
protected function generateValidationResponse($form, $e)
{
$controller = $this->getToplevelController();
$form->sessionMessage($e->getResult()->message(), 'bad', false);
$responseNegotiator = new PjaxResponseNegotiator(array(
'CurrentForm' => function () use (&$form) {
return $form->forTemplate();
},
'default' => function () use (&$controller) {
return $controller->redirectBack();
}
));
if ($controller->getRequest()->isAjax()) {
$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
}
return $responseNegotiator->respond($controller->getRequest());
}
/** /**
* @param array $data * @param array $data
* @param Form $form * @param Form $form
* @return HTTPResponse * @return HTTPResponse
* @throws ValidationException
*/ */
public function doDelete($data, $form) public function doDelete($data, $form)
{ {
$title = $this->record->Title; $title = $this->record->Title;
try {
if (!$this->record->canDelete()) { if (!$this->record->canDelete()) {
throw new ValidationException( throw new ValidationException(
_t('GridFieldDetailForm.DeletePermissionsFailure',"No delete permissions")); _t('GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
);
} }
$this->record->delete(); $this->record->delete();
} catch (ValidationException $e) {
$form->sessionMessage($e->getResult()->message(), 'bad', false);
return $this->getToplevelController()->redirectBack();
}
$message = sprintf( $message = sprintf(
_t('GridFieldDetailForm.Deleted', 'Deleted %s %s'), _t('GridFieldDetailForm.Deleted', 'Deleted %s %s'),
@ -542,9 +506,9 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
$toplevelController = $this->getToplevelController(); $toplevelController = $this->getToplevelController();
if ($toplevelController && $toplevelController instanceof LeftAndMain) { if ($toplevelController && $toplevelController instanceof LeftAndMain) {
$backForm = $toplevelController->getEditForm(); $backForm = $toplevelController->getEditForm();
$backForm->sessionMessage($message, 'good', false); $backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
} else { } else {
$form->sessionMessage($message, 'good', false); $form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
} }
//when an item is deleted, redirect to the parent controller //when an item is deleted, redirect to the parent controller
@ -581,7 +545,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
{ {
$templates = SSViewer::get_templates_by_class($this, '', __CLASS__); $templates = SSViewer::get_templates_by_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;

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,7 +88,10 @@ class RequiredFields extends Validator
$valid = ($field->validate($this) && $valid); $valid = ($field->validate($this) && $valid);
} }
if ($this->required) { if (!$this->required) {
return $valid;
}
foreach ($this->required as $fieldName) { foreach ($this->required as $fieldName) {
if (!$fieldName) { if (!$fieldName) {
continue; continue;
@ -135,7 +142,6 @@ class RequiredFields extends Validator
$valid = false; $valid = false;
} }
} }
}
return $valid; return $valid;
} }

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

@ -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;

View File

@ -109,7 +109,7 @@ class Hierarchy extends DataExtension
// 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(
@ -158,16 +158,16 @@ class Hierarchy extends DataExtension
$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;
@ -175,7 +175,7 @@ class Hierarchy extends DataExtension
} }
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;
@ -187,17 +187,17 @@ class Hierarchy extends DataExtension
} }
$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;");
@ -214,7 +214,7 @@ class Hierarchy extends DataExtension
) { ) {
// 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 {
@ -229,7 +229,7 @@ class Hierarchy extends DataExtension
$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();
} }
@ -240,7 +240,7 @@ class Hierarchy extends DataExtension
$output .= "</ul>\n"; $output .= "</ul>\n";
} }
if(isset($foundAChild) && $foundAChild) { if (isset($foundAChild) && $foundAChild) {
return $output; return $output;
} }
return null; return null;
@ -275,9 +275,9 @@ class Hierarchy extends DataExtension
$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) {
@ -325,16 +325,16 @@ class Hierarchy extends DataExtension
*/ */
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;
} }
} }
@ -342,7 +342,7 @@ class Hierarchy extends DataExtension
} 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);
} }
} }
@ -362,7 +362,7 @@ class Hierarchy extends DataExtension
$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;
@ -374,12 +374,12 @@ class Hierarchy extends DataExtension
} }
$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();
@ -401,9 +401,9 @@ class Hierarchy extends DataExtension
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();
} }
} }
@ -420,14 +420,14 @@ class Hierarchy extends DataExtension
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";
@ -444,9 +444,9 @@ class Hierarchy extends DataExtension
*/ */
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;
@ -462,9 +462,9 @@ class Hierarchy extends DataExtension
*/ */
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);
} }
} }
@ -489,7 +489,7 @@ class Hierarchy extends DataExtension
{ {
$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;
} }
@ -547,7 +547,7 @@ class Hierarchy extends DataExtension
*/ */
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]);
} }
} }
@ -607,9 +607,9 @@ class Hierarchy extends DataExtension
*/ */
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;
@ -629,7 +629,7 @@ class Hierarchy extends DataExtension
*/ */
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) {
@ -680,14 +680,14 @@ class Hierarchy extends DataExtension
} }
$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);
@ -714,7 +714,7 @@ class Hierarchy extends DataExtension
*/ */
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');
} }
@ -735,7 +735,7 @@ class Hierarchy extends DataExtension
*/ */
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');
} }
@ -752,7 +752,7 @@ class Hierarchy extends DataExtension
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();
@ -816,7 +816,7 @@ class Hierarchy extends DataExtension
*/ */
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');
} }
@ -836,7 +836,7 @@ class Hierarchy extends DataExtension
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);
} }
@ -853,7 +853,7 @@ class Hierarchy extends DataExtension
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');
@ -873,7 +873,7 @@ class Hierarchy extends DataExtension
$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);
} }
@ -923,8 +923,8 @@ class Hierarchy extends DataExtension
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;
} }
} }
@ -944,22 +944,22 @@ class Hierarchy extends DataExtension
"\"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;

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,6 +13,7 @@ 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
@ -23,52 +26,40 @@ class ValidationException extends Exception
* 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
* the error code number.
* @param integer $code The error code number, if not given in the second parameter
*/ */
public function __construct($result = null, $code = 0, $dummy = null) { public function __construct($result = null, $code = 0)
$exceptionMessage = null; {
// Catch legacy behaviour where second argument was not code
// Backwards compatibiliy failover. The 2nd argument used to be $message, and $code the 3rd. if ($code && !is_numeric($code)) {
// For callers using that, we ditch the message throw new InvalidArgumentException("Code must be numeric");
if(!is_numeric($code)) {
$exceptionMessage = $code;
if($dummy) $code = $dummy;
} }
if($result instanceof ValidationResult) { // Set default message and result
$exceptionMessage = _t("ValidationException.DEFAULT_ERROR", "Validation error");
if (!$result) {
$result = $exceptionMessage;
}
// Check result type
if ($result instanceof ValidationResult) {
$this->result = $result; $this->result = $result;
// Pick first message
} else if(is_string($result)) { foreach ($result->getMessages() as $message) {
$exceptionMessage = $message['message'];
break;
}
} elseif (is_string($result)) {
$this->result = ValidationResult::create()->addError($result); $this->result = ValidationResult::create()->addError($result);
$exceptionMessage = $result;
} else if(!$result) {
$this->result = ValidationResult::create()->addError(_t("ValdiationExcetpion.DEFAULT_ERROR", "Validation error"));
} else { } else {
throw new InvalidArgumentException( throw new InvalidArgumentException(
"ValidationExceptions must be passed a ValdiationResult, a string, or nothing at all"); "ValidationExceptions must be passed a ValdiationResult, a string, or nothing at all"
);
} }
// Construct parent::__construct($exceptionMessage, $code);
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);
} }
/** /**

View File

@ -2,92 +2,99 @@
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 * Standard "error" type
*/
const TYPE_ERROR = 'error';
/**
* Standard "good" message type
*/
const TYPE_GOOD = 'good';
/**
* Non-error message type.
*/
const TYPE_INFO = 'info';
/**
* Warning message type
*/
const TYPE_WARNING = 'warning';
/**
* Message type is html
*/
const CAST_HTML = 'html';
/**
* 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; protected $isValid = true;
/** /**
* @var array of errors * List of messages
*
* @var array
*/ */
protected $errorList = array(); protected $messages = array();
/** /**
* Create a new ValidationResult. * Create a new ValidationResult.
* By default, it is a successful result. Call $this->error() to record errors. * By default, it is a successful result. Call $this->error() to record errors.
*
* @param void $valid @deprecated
* @param void $message @deprecated
*/ */
public function __construct($valid = null, $message = null) { public function __construct()
if ($message !== null) { {
Deprecation::notice('3.2', '$message parameter is deprecated please use addMessage or addError instead', false); if (func_num_args() > 0) {
$this->addError($message);
}
if ($valid !== null) {
Deprecation::notice('3.2', '$valid parameter is deprecated please addError to mark the result as invalid', false); Deprecation::notice('3.2', '$valid parameter is deprecated please addError to mark the result as invalid', false);
$this->isValid = $valid; $this->isValid = func_get_arg(0);
if ($message) {
$this->errorList[] = $message;
} }
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, * Record an error against this validation result,
* *
* @param string $message The message string. * @param string $message The message string.
* @param string $messageType The type of message: e.g. "bad", "warning", "good", or "required". Passed as a CSS * @param string $messageType Passed as a CSS class to the form, so other values can be used if desired.
* 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. * @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 * This can be usedful for ensuring no duplicate messages
* @param bool $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. * @return $this
*/ */
public function addError($message, $messageType = "bad", $code = null, $escapeHtml = true) { public function addError($message, $messageType = self::TYPE_ERROR, $code = null, $cast = self::CAST_TEXT)
{
return $this->addFieldError(null, $message, $messageType, $code, $escapeHtml); return $this->addFieldError(null, $message, $messageType, $code, $cast);
} }
/** /**
@ -99,14 +106,19 @@ class ValidationResult extends Object
* class to the form, so other values can be used if desired. * 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. * @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 * This can be usedful for ensuring no duplicate messages
* @param bool $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. * @return $this
*/ */
public function addFieldError($fieldName = null, $message, $messageType = "bad", $code = null, $escapeHtml = true) { public function addFieldError(
$fieldName,
$message,
$messageType = self::TYPE_ERROR,
$code = null,
$cast = self::CAST_TEXT
) {
$this->isValid = false; $this->isValid = false;
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast);
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $escapeHtml);
} }
/** /**
@ -117,12 +129,13 @@ class ValidationResult extends Object
* class to the form, so other values can be used if desired. * 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. * @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 * This can be usedful for ensuring no duplicate messages
* @param bool $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. * @return $this
*/ */
public function addMessage($message, $messageType = "bad", $code = null, $escapeHtml = true) { public function addMessage($message, $messageType = self::TYPE_ERROR, $code = null, $cast = self::CAST_TEXT)
return $this->addFieldMessage(null, $message, $messageType, $code, $escapeHtml); {
return $this->addFieldMessage(null, $message, $messageType, $code, $cast);
} }
/** /**
@ -134,26 +147,34 @@ class ValidationResult extends Object
* class to the form, so other values can be used if desired. * 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. * @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 * This can be usedful for ensuring no duplicate messages
* @param bool $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. * @return $this
*/ */
public function addFieldMessage($fieldName, $message, $messageType = "bad", $code = null, $escapeHtml = true) { 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( $metadata = array(
'message' => $escapeHtml ? Convert::raw2xml($message) : $message, 'message' => $message,
'fieldName' => $fieldName, 'fieldName' => $fieldName,
'messageType' => $messageType, 'messageType' => $messageType,
'messageCast' => $cast,
); );
if($code) { if ($code) {
if(!is_numeric($code)) { $this->messages[$code] = $metadata;
$this->errorList[$code] = $metadata;
} else { } else {
throw new InvalidArgumentException( $this->messages[] = $metadata;
"ValidationResult::error() - Don't use a numeric code '$code'. Use a string.");
}
} else {
$this->errorList[] = $metadata;
} }
return $this; return $this;
@ -163,84 +184,19 @@ class ValidationResult extends Object
* Returns true if the result is valid. * Returns true if the result is valid.
* @return boolean * @return boolean
*/ */
public function valid() public function isValid()
{ {
return $this->isValid; return $this->isValid;
} }
/** /**
* Get an array of errors * Return the full error meta-data, suitable for combining with another ValidationResult.
* @return array *
* @return array Array of messages, where each item is an array of data for that message.
*/ */
public function messageList() public function getMessages()
{ {
$list = array(); return $this->messages;
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.
* @return string
*/
public function message()
{
return implode("; ", $this->messageList());
}
/**
* The the error message that's not related to a field as a string
*/
public function overallMessage() {
$messages = array();
foreach($this->errorList as $item) {
if(!$item['fieldName']) $messages[] = $item['message'];
}
return implode("; ", $messages);
}
/**
* Get a starred list of all messages
* @return string
*/
public function starredList()
{
return " * " . implode("\n * ", $this->messageList());
} }
/** /**
@ -253,8 +209,28 @@ class ValidationResult extends Object
*/ */
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,15 +108,32 @@ 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()) { // Fail if passwords do not match
if ($data['NewPassword1'] !== $data['NewPassword2']) {
$this->clearMessage();
$this->sessionMessage(
_t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$this->setSessionValidationResult($validationResult);
return $this->controller->redirect($this->controller->Link('changepassword'));
}
// Clear locked out status // Clear locked out status
$member->LockedOutUntil = null; $member->LockedOutUntil = null;
$member->FailedLoginCount = null; $member->FailedLoginCount = null;
$member->write(); $member->write();
if ($member->canLogIn()->valid()) { if ($member->canLogIn()->isValid()) {
$member->logIn(); $member->logIn();
} }
@ -138,30 +155,5 @@ class ChangePasswordForm extends Form
); );
return $this->controller->redirect($redirectURL); 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->sessionMessage(
_t('Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->controller->redirect($this->controller->Link('changepassword'));
}
} }
} }

View File

@ -96,7 +96,7 @@ class Group extends DataObject
$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());
} }
@ -148,7 +148,7 @@ class Group extends DataObject
// 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'));
@ -164,16 +164,16 @@ class Group extends DataObject
$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(
@ -183,7 +183,7 @@ class Group extends DataObject
} }
} }
}); });
$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'));
@ -193,7 +193,7 @@ class Group extends DataObject
// 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(
@ -205,13 +205,13 @@ class Group extends DataObject
); );
} }
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',
@ -236,14 +236,14 @@ class Group extends DataObject
// 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);
@ -260,7 +260,7 @@ class Group extends DataObject
->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);
@ -285,7 +285,7 @@ class Group extends DataObject
$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');
@ -315,8 +315,8 @@ class Group extends DataObject
// 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');
}); });
@ -351,8 +351,8 @@ class Group extends DataObject
$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
@ -371,7 +371,7 @@ class Group extends DataObject
{ {
$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;
} }
@ -399,7 +399,7 @@ class Group extends DataObject
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);
@ -422,12 +422,12 @@ class Group extends DataObject
// 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',
@ -448,7 +448,7 @@ class Group extends DataObject
// 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);
} }
} }
@ -458,12 +458,12 @@ class Group extends DataObject
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();
} }
} }
@ -587,7 +587,7 @@ class Group extends DataObject
// 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');
@ -601,7 +601,7 @@ class Group extends DataObject
// 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');

View File

@ -285,7 +285,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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
@ -296,7 +296,7 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
// 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
@ -318,24 +318,24 @@ class Member extends DataObject implements TemplateGlobalProvider
$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.'
)); ));
@ -367,7 +367,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$result = ValidationResult::create(); $result = ValidationResult::create();
if($this->isLockedOut()) { if ($this->isLockedOut()) {
$result->addError( $result->addError(
_t( _t(
'Member.ERRORLOCKEDOUT2', 'Member.ERRORLOCKEDOUT2',
@ -469,7 +469,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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',
@ -534,8 +534,8 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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;
} }
@ -558,7 +558,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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()
@ -566,7 +566,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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) {
@ -582,7 +582,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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(
@ -590,7 +590,7 @@ class Member extends DataObject implements TemplateGlobalProvider
'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
@ -603,11 +603,11 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
} }
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);
} }
@ -707,7 +707,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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
))); )));
@ -771,7 +771,7 @@ class Member extends DataObject implements TemplateGlobalProvider
->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());
} }
@ -791,7 +791,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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()
@ -826,7 +826,7 @@ class Member extends DataObject implements TemplateGlobalProvider
); );
// 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);
} }
@ -865,7 +865,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$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);
} }
} }
@ -878,7 +878,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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");
} }
@ -898,14 +898,14 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$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 {
@ -929,16 +929,16 @@ class Member extends DataObject implements TemplateGlobalProvider
// 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',
@ -947,7 +947,7 @@ class Member extends DataObject implements TemplateGlobalProvider
'name' => $identifierField, 'name' => $identifierField,
'value' => $this->$identifierField 'value' => $this->$identifierField
) )
))); ));
} }
} }
@ -970,7 +970,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// 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 = '';
@ -989,9 +989,9 @@ class Member extends DataObject implements TemplateGlobalProvider
$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;
@ -1000,7 +1000,7 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
// save locale // save locale
if(!$this->Locale) { if (!$this->Locale) {
$this->Locale = i18n::get_locale(); $this->Locale = i18n::get_locale();
} }
@ -1013,7 +1013,7 @@ class Member extends DataObject implements TemplateGlobalProvider
Permission::flush_permission_cache(); Permission::flush_permission_cache();
if($this->isChanged('Password')) { if ($this->isChanged('Password')) {
MemberPassword::log($this); MemberPassword::log($this);
} }
} }
@ -1050,7 +1050,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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;
} }
@ -1091,13 +1091,13 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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);
@ -1132,7 +1132,7 @@ class Member extends DataObject implements TemplateGlobalProvider
'"Group"."Code"' => $groupcode '"Group"."Code"' => $groupcode
)); ));
if($group) { if ($group) {
$this->Groups()->add($group); $this->Groups()->add($group);
} else { } else {
if (!$title) { if (!$title) {
@ -1157,7 +1157,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$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);
} }
} }
@ -1191,7 +1191,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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);
@ -1199,13 +1199,13 @@ class Member extends DataObject implements TemplateGlobalProvider
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;
} }
} }
@ -1232,7 +1232,7 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
$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);
} }
@ -1288,7 +1288,7 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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');
@ -1304,7 +1304,7 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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');
@ -1352,24 +1352,24 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$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());
} }
@ -1390,7 +1390,7 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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')) {
@ -1399,7 +1399,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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)));
} }
@ -1414,11 +1414,11 @@ class Member extends DataObject implements TemplateGlobalProvider
$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;
} }
@ -1426,7 +1426,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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
@ -1454,8 +1454,8 @@ class Member extends DataObject implements TemplateGlobalProvider
$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]);
} }
@ -1477,7 +1477,7 @@ class Member extends DataObject implements TemplateGlobalProvider
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();
@ -1491,7 +1491,7 @@ class Member extends DataObject implements TemplateGlobalProvider
)); ));
$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');
} }
@ -1505,9 +1505,9 @@ class Member extends DataObject implements TemplateGlobalProvider
$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(' > ');
} }
@ -1526,7 +1526,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// 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,
@ -1581,7 +1581,7 @@ class Member extends DataObject implements TemplateGlobalProvider
) )
); );
$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);
}); });
@ -1606,7 +1606,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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',
@ -1627,21 +1627,21 @@ class Member extends DataObject implements TemplateGlobalProvider
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
@ -1658,26 +1658,26 @@ class Member extends DataObject implements TemplateGlobalProvider
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
@ -1692,30 +1692,30 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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;
} }
} }
@ -1730,14 +1730,14 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$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));
} }
} }
@ -1757,7 +1757,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$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();
} }
@ -1771,11 +1771,11 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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;
@ -1790,7 +1790,7 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
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();
@ -1808,11 +1808,11 @@ class Member extends DataObject implements TemplateGlobalProvider
$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');
} }

View File

@ -44,7 +44,7 @@ class MemberAuthenticator extends Authenticator
// 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) {
@ -54,24 +54,24 @@ class MemberAuthenticator extends Authenticator
// 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)
@ -79,17 +79,21 @@ class MemberAuthenticator extends Authenticator
} }
// 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();
}
if ($form) {
$form->setSessionValidationResult($result, true);
}
} else { } else {
if ($member) { if ($member) {
$member->registerSuccessfulLogin(); $member->registerSuccessfulLogin();
@ -116,12 +120,12 @@ class MemberAuthenticator extends Authenticator
// 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';
@ -131,7 +135,7 @@ class MemberAuthenticator extends Authenticator
} 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');
@ -170,7 +174,7 @@ class MemberAuthenticator extends Authenticator
// 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();

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

@ -78,8 +78,8 @@ class PasswordValidator extends Object
{ {
$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(
@ -94,11 +94,11 @@ class PasswordValidator extends Object
} }
} }
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(
@ -109,7 +109,7 @@ class PasswordValidator extends Object
} }
} }
if($score < $this->minScore) { if ($score < $this->minScore) {
$valid->addError( $valid->addError(
sprintf( sprintf(
_t( _t(
@ -124,14 +124,14 @@ class PasswordValidator extends Object
} }
} }
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',

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

@ -33,16 +33,6 @@ class HierarchyTest extends SapphireTest {
$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.');
}
/** /**
* Test Hierarchy::AllHistoricalChildren(). * Test Hierarchy::AllHistoricalChildren().
@ -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('Invalid type')
->addError('Out of kiwis'); ->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',
}
/**
* Test that a ValidationException created with no contained ValidationResult
* will correctly populate itself with an inferred version
*/
public function testCreateForField() {
$exception = ValidationException::create_for_field('Content', 'Content is required');
$this->assertEquals('Content is required', $exception->getMessage());
$this->assertEquals(false, $exception->getResult()->valid());
$this->assertEquals(array(
'Content' => array(
'message' => 'Content is required',
'messageType' => 'bad', 'messageType' => 'bad',
), 'messageCast' => ValidationResult::CAST_TEXT,
), $exception->getResult()->fieldErrors()); 'fieldName' => null,
],
'BECLEAN' => [
'message' => 'You didn\'t wash your hands',
'messageType' => 'bad',
'messageCast' => ValidationResult::CAST_HTML,
'fieldName' => null,
],
],
$result->getMessages()
);
} }
/** /**
@ -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);
/** @var Member $member */
$member = DataObject::get_by_id(Member::class, $member->ID); $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,6 +251,7 @@ class MemberTest extends FunctionalTest {
* - at least 7 characters long * - at least 7 characters long
*/ */
public function testValidatePassword() { public function testValidatePassword() {
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
@ -257,73 +259,73 @@ class MemberTest extends FunctionalTest {
// 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() {
@ -641,11 +642,34 @@ class SecurityTest extends FunctionalTest {
} }
/** /**
* Get the error message on the login form * Assert this message is in the current login form errors
*
* @param string $expected
* @param string $errorMessage
*/ */
public function loginErrorMessage() { protected function assertHasMessage($expected, $errorMessage = null) {
$result = $this->session()->inst_get('FormInfo.MemberLoginForm_LoginForm.result'); $messages = [];
return $result->message(); $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;
}
} }