").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
diff --git a/examples/demo/site/themes/bootstrap/assets/js/popup_search.js b/examples/demo/site/themes/bootstrap/assets/js/popup_search.js
new file mode 100644
index 0000000..5854223
--- /dev/null
+++ b/examples/demo/site/themes/bootstrap/assets/js/popup_search.js
@@ -0,0 +1,8 @@
+ $(document).ready(function() {
+ $('#gcse_search_form').submit(function() {
+ window.open('', 'formpopup', 'width=600,height=600,resizeable,scrollbars');
+ this.target = 'formpopup';
+ });
+ });
+
+
diff --git a/examples/demo/site/themes/bootstrap/assets/scss/style.scss b/examples/demo/site/themes/bootstrap/assets/scss/style.scss
new file mode 100644
index 0000000..90d2bb9
--- /dev/null
+++ b/examples/demo/site/themes/bootstrap/assets/scss/style.scss
@@ -0,0 +1,107 @@
+ul.horizontal {
+ li {
+ display: inline-block;
+ }
+}
+
+#header {
+ #primary.menu {
+ ul {
+ li {
+ color: #555;
+ a {
+ color: #555;
+ text-decoration: none;
+ &:hover { color: black; }
+ }
+ background-color: #fff;
+ padding: 10px;
+ margin: 0;
+ }
+ &.horizontal {
+ border-bottom: solid 1px #ddd;
+ li {
+ border-top: solid 3px #fff;
+ &:hover {
+ background-color: #ffe;
+ border-top: solid 3px #999;
+ }
+ &.active {
+ font-weight: bold;
+ border-top: solid 3px #ddd;
+ background-color: #ddd;
+ }
+ &.active:hover {
+ border-top: solid 3px blue;
+ }
+ }
+ }
+ }
+ }
+}
+#content {
+ margin-left: 20px;
+ #highlighted {
+ position: relative;
+ border: solid 1px #ddd;
+ background-color: #ffc;
+ width: 70%;
+ left: 15%;
+ right: 15%;
+ padding: 5px;
+ font-style: italic;
+ }
+}
+.sidebar {
+ padding: 5px;
+ margin: 3px;
+ /* border: solid 1px #ccc; */
+ sidebar_first {
+ width: 250px;
+ position: fixed;
+ top: 45px;
+ left: 0;
+ bottom: 0;
+ width: 200px;
+ border-right: solid 1px #ddd;
+ }
+ sidebar_second {
+ width: 250px;
+ float: right;
+ }
+
+ &+.main {
+ margin-left: 200px;
+ }
+}
+#primary-tabs {
+ ul.horizontal {
+ list-style-type: none;
+ li {
+ display: inline;
+ padding: 2px 5px;
+ border: solid 1px #ccf;
+ }
+ li.active {
+ border-color: #99f #99f #ddd;
+ border-style: solid solid none;
+ border-width: 2px 1px 0;
+ padding: 2px 7px 1px;
+ }
+ }
+}
+#message li.error {
+ background-color: #f99;
+ border: solid 1px red;
+ padding: 5px 2px 5px 2px;
+}
+
+table.with_border {
+ thead td {
+ font-weight: bold;
+ }
+ td {
+ border: solid 1px #ccc;
+ padding: 2px 5px 2px 5px;
+ }
+}
diff --git a/examples/demo/site/themes/bootstrap/debug.tpl b/examples/demo/site/themes/bootstrap/debug.tpl
new file mode 100644
index 0000000..b689091
--- /dev/null
+++ b/examples/demo/site/themes/bootstrap/debug.tpl
@@ -0,0 +1,38 @@
+{assign name="debug_enabled" value="True"/}
+{if condition="$debug_enabled"}
+
+{literal}
+
+{/literal}
+
Show debug
+
+{assign name="kpage" value="page"/}{assign name="kregions" value="regions"/}{foreach key="k" item="i" from="$page.variables"}{unless condition="$k ~ $kpage"}{unless condition="$k ~ $kregions"}{$k/} ={htmlentities}{$i/}{/htmlentities} {/unless}{/unless}
+{/foreach}
+
+
+
+{/if}
diff --git a/examples/demo/site/themes/bootstrap/page.tpl b/examples/demo/site/themes/bootstrap/page.tpl
new file mode 100644
index 0000000..3205841
--- /dev/null
+++ b/examples/demo/site/themes/bootstrap/page.tpl
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+{if isset="$head"}{$head/}{/if}
+{if isset="$styles"}{$styles/}{/if}
+{if isset="$scripts"}{$scripts/}{/if}
+{if isset="$head_lines"}{$head_lines/}{/if}
+
+
+
+
+
+
+
+
{$head_title/}
+
+
+
+ {if isset="$region_top"}
+ {$region_top/}
+ {/if}
+
+
+
+
+
+
+
+
+
+
+ {unless isempty="$page.region_sidebar_first"}
+
+ {/unless}
+
+ {unless isempty="$page.region_sidebar_second"}
+
+ {/unless}
+
+
+
+
+ {unless isempty="$page.region_highlighted"}
+
{$page.region_highlighted/}
+ {/unless}
+
+ {unless isempty="$page.region_help"}
+
{$page.region_help/}
+ {/unless}
+
+
+ {unless isempty="$page_title"}
{$page_title/} {/unless}
+ {$page.region_content/}
+ {if condition="$page.is_front"}
+ {if isset="$page.region_feed_news"}
+
{$page.region_feed_news/}
+ {/if}
+ {if isset="$page.region_feed_forum"}
+
{$page.region_feed_forum/}
+ {/if}
+ {/if}
+
+
+
+
+
+ {$page.region_footer/}
+
+
+ {$page.region_bottom/}
+
+
+
+
+
+
+{include file="debug.tpl"/}
+
+
diff --git a/examples/demo/site/themes/bootstrap/theme.info b/examples/demo/site/themes/bootstrap/theme.info
new file mode 100644
index 0000000..6c96570
--- /dev/null
+++ b/examples/demo/site/themes/bootstrap/theme.info
@@ -0,0 +1,14 @@
+name=bootstrap
+engine=smarty
+author=jvelilla
+version=0.1
+regions[page_top] = Top
+regions[header] = Header
+regions[content] = Content
+regions[highlighted] = Highlighted
+regions[help] = Help
+regions[footer] = Footer
+regions[sidebar_first] = first sidebar
+regions[sidebar_second] = second sidebar
+regions[page_bottom] = Bottom
+navigation=default_nav
diff --git a/examples/demo/src/demo_cms_execution.e b/examples/demo/src/demo_cms_execution.e
new file mode 100644
index 0000000..c73948f
--- /dev/null
+++ b/examples/demo/src/demo_cms_execution.e
@@ -0,0 +1,97 @@
+note
+ description: "[
+ CMS Execution for the demo server.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ DEMO_CMS_EXECUTION
+
+inherit
+ CMS_EXECUTION
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ initial_cms_setup: CMS_DEFAULT_SETUP
+ -- CMS setup.
+ local
+ l_env: CMS_ENVIRONMENT
+ do
+ if attached execution_environment.arguments.separate_character_option_value ('d') as l_dir then
+ create l_env.make_with_directory_name (l_dir)
+ else
+ create l_env.make_default
+ end
+ create Result.make (l_env)
+ end
+
+feature -- CMS storage
+
+ setup_storage (a_setup: CMS_SETUP)
+ do
+ a_setup.storage_drivers.force (create {CMS_STORAGE_SQLITE3_BUILDER}.make, "sqlite3")
+-- a_setup.storage_drivers.force (create {CMS_STORAGE_STORE_MYSQL_BUILDER}.make, "mysql")
+ a_setup.storage_drivers.force (create {CMS_STORAGE_STORE_ODBC_BUILDER}.make, "odbc")
+ end
+
+feature -- CMS modules
+
+ setup_modules (a_setup: CMS_SETUP)
+ -- Setup additional modules.
+ local
+ m: CMS_MODULE
+ do
+ create {CMS_ADMIN_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Auth
+ create {CMS_AUTHENTICATION_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {CMS_BASIC_AUTH_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {CMS_OAUTH_20_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {CMS_OPENID_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Nodes
+ create {CMS_NODE_MODULE} m.make (a_setup)
+ a_setup.register_module (m)
+
+ create {CMS_BLOG_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Taxonomy
+ create {CMS_TAXONOMY_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Recent changes
+ create {CMS_RECENT_CHANGES_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Recent changes
+ create {FEED_AGGREGATOR_MODULE} m.make
+ a_setup.register_module (m)
+
+ -- Miscellanious
+ create {CMS_DEBUG_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {CMS_DEMO_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {GOOGLE_CUSTOM_SEARCH_MODULE} m.make
+ a_setup.register_module (m)
+
+ create {CMS_SESSION_AUTH_MODULE} m.make
+ a_setup.register_module (m)
+ end
+
+end
diff --git a/examples/demo/src/demo_cms_server.e b/examples/demo/src/demo_cms_server.e
new file mode 100644
index 0000000..1e50b91
--- /dev/null
+++ b/examples/demo/src/demo_cms_server.e
@@ -0,0 +1,18 @@
+note
+ description: "[
+ DEMO application server.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ DEMO_CMS_SERVER
+
+inherit
+ ROC_CMS_LAUNCHER [DEMO_CMS_EXECUTION]
+
+create
+ make_and_launch
+
+end
+
diff --git a/launcher/README.txt b/launcher/README.txt
new file mode 100644
index 0000000..b63b6e1
--- /dev/null
+++ b/launcher/README.txt
@@ -0,0 +1,10 @@
+Collection of ROC CMS launcher ready to use.
+
+- Include any-safe.ecf to use any of cgi, libfcgi or standalone connector.
+- Include standalone-safe.ecf to use standalone connector.
+- Include libfcgi-safe.ecf to use libfcgi connector.
+- Include cgi-safe.ecf to use cgi connector.
+
+In application, the root class need to inherit from ROC_CMS_LAUNCHER with adapted CMS_EXECUTION
+descendant.
+
diff --git a/launcher/any-safe.ecf b/launcher/any-safe.ecf
new file mode 100644
index 0000000..4a7326a
--- /dev/null
+++ b/launcher/any-safe.ecf
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/any.ecf b/launcher/any.ecf
new file mode 100644
index 0000000..9983098
--- /dev/null
+++ b/launcher/any.ecf
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/any/application_launcher.e b/launcher/any/application_launcher.e
new file mode 100644
index 0000000..bb184d0
--- /dev/null
+++ b/launcher/any/application_launcher.e
@@ -0,0 +1,19 @@
+note
+ description: "[
+ Effective class for APPLICATION_LAUNCHER_I
+
+ You can put modification in this class
+ ]"
+ date: "$Date: 2013-06-12 13:55:42 +0200 (mer., 12 juin 2013) $"
+ revision: "$Revision: 36 $"
+
+class
+ APPLICATION_LAUNCHER [G -> WSF_EXECUTION create make end]
+
+inherit
+ APPLICATION_LAUNCHER_I [G]
+
+feature -- Custom
+
+end
+
diff --git a/launcher/any/application_launcher_i.e b/launcher/any/application_launcher_i.e
new file mode 100644
index 0000000..026d12f
--- /dev/null
+++ b/launcher/any/application_launcher_i.e
@@ -0,0 +1,127 @@
+note
+ description: "[
+ Specific application launcher
+
+ DO NOT EDIT THIS CLASS
+
+ you can customize APPLICATION_LAUNCHER
+ ]"
+ date: "$Date: 2013-06-12 13:55:42 +0200 (mer., 12 juin 2013) $"
+ revision: "$Revision: 36 $"
+
+deferred class
+ APPLICATION_LAUNCHER_I [G -> WSF_EXECUTION create make end]
+
+inherit
+ SHARED_EXECUTION_ENVIRONMENT
+
+feature -- Execution
+
+ launch (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ nature: like launcher_nature
+ do
+ nature := launcher_nature
+ if nature = Void then
+ launch_standalone (opts)
+ elseif nature = nature_standalone then
+ launch_standalone (opts)
+ elseif nature = nature_nino then
+ launch_nino (opts)
+ elseif nature = nature_cgi then
+ launch_cgi (opts)
+ elseif nature = nature_libfcgi then
+ launch_libfcgi (opts)
+ else
+ -- bye bye
+ (create {EXCEPTIONS}).die (-1)
+ end
+ end
+
+feature {NONE} -- Access
+
+ launcher_nature: detachable READABLE_STRING_8
+ -- Initialize the launcher nature
+ -- either cgi, libfcgi, or nino.
+ --| We could extend with more connector if needed.
+ --| and we could use WSF_DEFAULT_SERVICE_LAUNCHER to configure this at compilation time.
+ local
+ p: PATH
+ ext: detachable READABLE_STRING_32
+ do
+ create p.make_from_string (execution_environment.arguments.command_name)
+ if attached p.entry as l_entry then
+ ext := l_entry.extension
+ end
+ if ext /= Void then
+ if ext.same_string (nature_standalone) then
+ Result := nature_standalone
+ end
+ if ext.same_string (nature_nino) then
+ Result := nature_nino
+ end
+ if ext.same_string (nature_cgi) then
+ Result := nature_cgi
+ end
+ if ext.same_string (nature_libfcgi) or else ext.same_string ("fcgi") then
+ Result := nature_libfcgi
+ end
+ end
+ Result := default_nature
+ end
+
+feature {NONE} -- standalone
+
+ nature_standalone: STRING = "standalone"
+
+ launch_standalone (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ launcher: WSF_STANDALONE_SERVICE_LAUNCHER [G]
+ do
+ create launcher.make_and_launch (opts)
+ end
+
+feature {NONE} -- nino
+
+ nature_nino: STRING = "nino"
+
+ launch_nino (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ launcher: WSF_NINO_SERVICE_LAUNCHER [G]
+ do
+ create launcher.make_and_launch (opts)
+ end
+
+feature {NONE} -- cgi
+
+ nature_cgi: STRING = "cgi"
+
+ launch_cgi (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ launcher: WSF_CGI_SERVICE_LAUNCHER [G]
+ do
+ create launcher.make_and_launch (opts)
+ end
+
+feature {NONE} -- libfcgi
+
+ nature_libfcgi: STRING = "libfcgi"
+
+ launch_libfcgi (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ launcher: WSF_LIBFCGI_SERVICE_LAUNCHER [G]
+ do
+ create launcher.make_and_launch (opts)
+ end
+
+feature -- Default
+
+ default_nature: STRING
+ do
+ Result := nature_standalone
+ end
+
+
+end
+
+
diff --git a/launcher/cgi-safe.ecf b/launcher/cgi-safe.ecf
new file mode 100644
index 0000000..678960d
--- /dev/null
+++ b/launcher/cgi-safe.ecf
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/default/application_launcher.e b/launcher/default/application_launcher.e
new file mode 100644
index 0000000..bb184d0
--- /dev/null
+++ b/launcher/default/application_launcher.e
@@ -0,0 +1,19 @@
+note
+ description: "[
+ Effective class for APPLICATION_LAUNCHER_I
+
+ You can put modification in this class
+ ]"
+ date: "$Date: 2013-06-12 13:55:42 +0200 (mer., 12 juin 2013) $"
+ revision: "$Revision: 36 $"
+
+class
+ APPLICATION_LAUNCHER [G -> WSF_EXECUTION create make end]
+
+inherit
+ APPLICATION_LAUNCHER_I [G]
+
+feature -- Custom
+
+end
+
diff --git a/launcher/default/application_launcher_i.e b/launcher/default/application_launcher_i.e
new file mode 100644
index 0000000..4d91129
--- /dev/null
+++ b/launcher/default/application_launcher_i.e
@@ -0,0 +1,26 @@
+note
+ description: "[
+ Specific application launcher
+
+ DO NOT EDIT THIS CLASS
+
+ you can customize APPLICATION_LAUNCHER
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+deferred class
+ APPLICATION_LAUNCHER_I [G -> WSF_EXECUTION create make end]
+
+feature -- Execution
+
+ launch (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ launcher: WSF_DEFAULT_SERVICE_LAUNCHER [G]
+ do
+ create launcher.make_and_launch (opts)
+ end
+
+end
+
+
diff --git a/launcher/libfcgi-safe.ecf b/launcher/libfcgi-safe.ecf
new file mode 100644
index 0000000..2e8fd5b
--- /dev/null
+++ b/launcher/libfcgi-safe.ecf
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/libfcgi.ecf b/launcher/libfcgi.ecf
new file mode 100644
index 0000000..71a51e6
--- /dev/null
+++ b/launcher/libfcgi.ecf
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/roc_cms_launcher.e b/launcher/roc_cms_launcher.e
new file mode 100644
index 0000000..e3de748
--- /dev/null
+++ b/launcher/roc_cms_launcher.e
@@ -0,0 +1,95 @@
+note
+ description: "[
+ Reusable ROC CMS launcher.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ ROC_CMS_LAUNCHER [G -> CMS_EXECUTION create make end]
+
+inherit
+ WSF_LAUNCHABLE_SERVICE
+ rename
+ make_and_launch as make_and_launch_service
+ redefine
+ initialize
+ end
+
+ REFACTORING_HELPER
+
+ SHARED_EXECUTION_ENVIRONMENT
+
+ SHARED_LOGGER
+
+create
+ make_and_launch
+
+feature {NONE} -- Initialization
+
+ make_and_launch
+ do
+ create launcher
+ make_and_launch_service
+ end
+
+ initialize
+ -- Initialize current service.
+ local
+ env: CMS_ENVIRONMENT
+ l_app_name: detachable READABLE_STRING_32
+ do
+ Precursor
+ create env.make_default
+ l_app_name := optional_application_name
+ if l_app_name = Void then
+ l_app_name := env.name
+ end
+ create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI} service_options.make_from_file (l_app_name + ".ini")
+ initialize_logger (env)
+ end
+
+ optional_application_name: detachable READABLE_STRING_32
+ -- Optional application name.
+ --| Redefine if needed.
+ do
+ end
+
+feature {NONE} -- Launch operation
+
+ launcher: APPLICATION_LAUNCHER [G]
+
+ launch (opts: detachable WSF_SERVICE_LAUNCHER_OPTIONS)
+ local
+ l_retry: BOOLEAN
+ l_message: STRING
+ do
+ if not l_retry then
+ launcher.launch (opts)
+ else
+ -- error hanling.
+ create l_message.make (1024)
+ if attached ((create {EXCEPTION_MANAGER}).last_exception) as l_exception then
+ if attached l_exception.description as l_description then
+ l_message.append (l_description.as_string_32)
+ l_message.append ("%N%N")
+ elseif attached l_exception.trace as l_trace then
+ l_message.append (l_trace)
+ l_message.append ("%N%N")
+ else
+ l_message.append (l_exception.out)
+ l_message.append ("%N%N")
+ end
+ else
+ l_message.append ("The application crashed without information.")
+ l_message.append ("%N%N")
+ end
+ -- send email shutdown
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+end
+
diff --git a/launcher/standalone-safe.ecf b/launcher/standalone-safe.ecf
new file mode 100644
index 0000000..092b77a
--- /dev/null
+++ b/launcher/standalone-safe.ecf
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/standalone.ecf b/launcher/standalone.ecf
new file mode 100644
index 0000000..f9aa378
--- /dev/null
+++ b/launcher/standalone.ecf
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/Readme.md b/library/Readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/library/app_env/Readme.md b/library/app_env/Readme.md
new file mode 100644
index 0000000..8b1c42a
--- /dev/null
+++ b/library/app_env/Readme.md
@@ -0,0 +1,13 @@
+Application Environment Library
+===============================
+
+Define a generic application environment to be re-used by different applications.
+
+site/
+ doc/
+ logs/
+ www/
+ assets
+ template
+ theme
+ config/
diff --git a/library/app_env/app_env-safe.ecf b/library/app_env/app_env-safe.ecf
new file mode 100644
index 0000000..462c9c3
--- /dev/null
+++ b/library/app_env/app_env-safe.ecf
@@ -0,0 +1,22 @@
+
+
+ Application Environment (layout, configuration, logger, database, ...)
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/app_env/app_env.ecf b/library/app_env/app_env.ecf
new file mode 100644
index 0000000..f7c8062
--- /dev/null
+++ b/library/app_env/app_env.ecf
@@ -0,0 +1,22 @@
+
+
+ Application Environment (layout, configuration, logger, database, ...)
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/app_env/license.lic b/library/app_env/license.lic
new file mode 100644
index 0000000..1f97942
--- /dev/null
+++ b/library/app_env/license.lic
@@ -0,0 +1,3 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR}, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
diff --git a/library/app_env/src/application_environment.e b/library/app_env/src/application_environment.e
new file mode 100644
index 0000000..c663b89
--- /dev/null
+++ b/library/app_env/src/application_environment.e
@@ -0,0 +1,211 @@
+note
+ description: "[
+ Application environment (layout, ...)
+ Related to file system locations such as
+ - configuration locations
+ - application
+ - log
+ - documentation
+ - www
+ - assets
+ - templates (html, Collection+JSON, ...)
+ - ...
+ ]"
+ date: "$Date: 2015-02-05 10:25:53 +0100 (jeu., 05 févr. 2015) $"
+ revision: "$Revision: 96584 $"
+
+class
+ APPLICATION_ENVIRONMENT
+
+inherit
+ SHARED_EXECUTION_ENVIRONMENT
+
+create
+ make_default,
+ make_with_path,
+ make_with_directory_name
+
+feature {NONE} -- Initialization
+
+ make_default
+ -- Create a default layout based on current working directory.
+ local
+ p: PATH
+ do
+ create p.make_current
+ p := p.extended ("site")
+ make_with_path (p)
+ end
+
+ make_with_path (p: PATH)
+ -- Create a layour based on a path `p'.
+ do
+ path := p.absolute_path.canonical_path
+ initialize_name
+ end
+
+ make_with_directory_name (a_dirname: READABLE_STRING_GENERAL)
+ -- Create a layour based on a path `p'.
+ do
+ make_with_path (create {PATH}.make_from_string (a_dirname))
+ end
+
+ initialize_name
+ -- Initialize `name'.
+ local
+ p: PATH
+ s: STRING_32
+ do
+ create p.make_from_string (execution_environment.arguments.command_name)
+ if attached p.entry as e then
+ p := e
+ end
+ create s.make_from_string (p.name)
+ if attached p.extension as l_extension then
+ s.remove_tail (l_extension.count + 1)
+ end
+ if s.is_whitespace then
+ set_name ({STRING_8} "app")
+ else
+ set_name (s)
+ end
+ end
+
+feature -- Access
+
+ path: PATH
+ -- Root location.
+
+ name: IMMUTABLE_STRING_32
+ -- Application name, default is "app"
+
+feature -- Change
+
+ set_name (a_name: READABLE_STRING_GENERAL)
+ -- Set `name' from `a_name'.
+ do
+ create name.make_from_string_general (a_name)
+ end
+
+feature -- Access: internal
+
+ config_path: PATH
+ -- Configuration file path.
+ local
+ p: detachable PATH
+ do
+ p := internal_config_path
+ if p = Void then
+ p := path.extended ("config")
+ internal_config_path := p
+ end
+ Result := p
+ end
+
+ application_config_path: PATH
+ -- Database Configuration file path.
+ local
+ p: detachable PATH
+ do
+ p := internal_application_config_path
+ if p = Void then
+ p := config_path.extended (name + ".json")
+ internal_application_config_path := p
+ end
+ Result := p
+ end
+
+ logs_path: PATH
+ -- Directory for logs.
+ local
+ p: detachable PATH
+ do
+ p := internal_logs_path
+ if p = Void then
+ p := path.extended ("logs")
+ internal_logs_path := p
+ end
+ Result := p
+ end
+
+ documentation_path: PATH
+ -- Directory for API documentation.
+ local
+ p: detachable PATH
+ do
+ p := internal_documentation_path
+ if p = Void then
+ p := path.extended ("doc")
+ internal_documentation_path := p
+ end
+ Result := p
+ end
+
+ www_path: PATH
+ -- Directory for www.
+ local
+ p: detachable PATH
+ do
+ p := internal_www_path
+ if p = Void then
+ p := path.extended ("www")
+ internal_www_path := p
+ end
+ Result := p
+ end
+
+ assets_path: PATH
+ -- Directory for public assets.
+ -- css, images, js.
+ local
+ p: detachable PATH
+ do
+ p := internal_assets_path
+ if p = Void then
+ p := www_path.extended ("assets")
+ internal_assets_path := p
+ end
+ Result := p
+ end
+
+ template_path: PATH
+ -- Directory for templates (HTML, etc).
+ local
+ p: detachable PATH
+ do
+ p := internal_template_path
+ if p = Void then
+ p := www_path.extended ("template")
+ internal_template_path := p
+ end
+ Result := p
+ end
+
+feature {NONE} -- Implementation
+
+ internal_config_path: detachable like config_path
+ -- Configuration file path.
+
+ internal_application_config_path: detachable like application_config_path
+ -- Database Configuration file path.
+
+ internal_logs_path: detachable like logs_path
+ -- Directory for logs.
+
+ internal_documentation_path: detachable like documentation_path
+ -- Directory for API documentation.
+
+ internal_www_path: detachable like www_path
+ -- Directory for www.
+
+ internal_assets_path: detachable like assets_path
+ -- Directory for public assets.
+ -- css, images, js.
+
+ internal_template_path: detachable like template_path
+ -- Directory for templates (HTML, etc).
+
+;note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/application_layout.e b/library/app_env/src/application_layout.e
new file mode 100644
index 0000000..32731ce
--- /dev/null
+++ b/library/app_env/src/application_layout.e
@@ -0,0 +1,23 @@
+note
+ description: "See {APPLICATION_ENVIRONMENT}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ APPLICATION_LAYOUT
+
+obsolete
+ "Use APPLICATION_ENVIRONMENT [April/2015]"
+
+inherit
+ APPLICATION_ENVIRONMENT
+
+create
+ make_default,
+ make_with_path,
+ make_with_directory_name
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/configuration/application_json_configuration_helper.e b/library/app_env/src/configuration/application_json_configuration_helper.e
new file mode 100644
index 0000000..2f2077a
--- /dev/null
+++ b/library/app_env/src/configuration/application_json_configuration_helper.e
@@ -0,0 +1,119 @@
+note
+ description: "Provide access to json configuration"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ APPLICATION_JSON_CONFIGURATION_HELPER
+
+feature -- Application Configuration
+
+ new_smtp_configuration (a_path: PATH): READABLE_STRING_32
+ -- Build a new database configuration.
+ local
+ l_parser: JSON_PARSER
+ do
+ Result := ""
+ if attached json_file_from (a_path) as json_file then
+ l_parser := new_json_parser (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached {JSON_OBJECT} l_parser.parsed_json_value as jv and then
+ attached {JSON_OBJECT} jv.item ("smtp") as l_smtp and then
+ attached {JSON_STRING} l_smtp.item ("server") as l_server
+ then
+ Result := l_server.item
+ end
+ end
+ end
+
+ new_database_configuration (a_path: PATH): detachable DATABASE_CONFIGURATION
+ -- Build a new database configuration.
+ local
+ l_parser: JSON_PARSER
+ do
+ if attached json_file_from (a_path) as json_file then
+ l_parser := new_json_parser (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached {JSON_OBJECT} l_parser.parsed_json_value as jv and then
+ attached {JSON_OBJECT} jv.item ("database") as l_database and then
+ attached {JSON_OBJECT} l_database.item ("datasource") as l_datasource and then
+ attached {JSON_STRING} l_datasource.item ("driver") as l_driver and then
+ attached {JSON_STRING} l_datasource.item ("environment") as l_envrionment and then
+ attached {JSON_OBJECT} l_database.item ("environments") as l_environments and then
+ attached {JSON_OBJECT} l_environments.item (l_envrionment.item) as l_environment_selected and then
+ attached {JSON_STRING} l_environment_selected.item ("connection_string") as l_connection_string
+ then
+ create Result.make (l_driver.item, l_connection_string.unescaped_string_32)
+ end
+ end
+ end
+
+ new_logger_level_configuration (a_path: PATH): READABLE_STRING_32
+ -- Retrieve a new logger level configuration.
+ -- By default, level is set to `DEBUG'.
+ local
+ l_parser: JSON_PARSER
+ do
+ Result := "DEBUG"
+ if attached json_file_from (a_path) as json_file then
+ l_parser := new_json_parser (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached {JSON_OBJECT} l_parser.parsed_json_value as jv and then
+ attached {JSON_OBJECT} jv.item ("logger") as l_logger and then
+ attached {JSON_STRING} l_logger.item ("level") as l_level
+ then
+ Result := l_level.item
+ end
+ end
+ end
+
+ new_database_configuration_test (a_path: PATH): detachable DATABASE_CONFIGURATION
+ -- Build a new database configuration for testing purposes.
+ local
+ l_parser: JSON_PARSER
+ do
+ if attached json_file_from (a_path) as json_file then
+ l_parser := new_json_parser (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached {JSON_OBJECT} l_parser.parsed_json_value as jv and then
+ l_parser.is_parsed and then
+ attached {JSON_OBJECT} jv.item ("database") as l_database and then
+ attached {JSON_OBJECT} l_database.item ("datasource") as l_datasource and then
+ attached {JSON_STRING} l_datasource.item ("driver") as l_driver and then
+ attached {JSON_STRING} l_datasource.item ("environment") as l_envrionment and then
+ attached {JSON_STRING} l_datasource.item ("trusted") as l_trusted and then
+ attached {JSON_OBJECT} l_database.item ("environments") as l_environments and then
+ attached {JSON_OBJECT} l_environments.item ("test") as l_environment_selected and then
+ attached {JSON_STRING} l_environment_selected.item ("connection_string") as l_connection_string and then
+ attached {JSON_STRING} l_environment_selected.item ("name") as l_name
+ then
+ create Result.make (l_driver.item, l_connection_string.unescaped_string_8)
+ end
+ end
+ end
+
+feature {NONE} -- JSON
+
+ json_file_from (a_fn: PATH): detachable STRING
+ do
+ Result := (create {JSON_FILE_READER}).read_json_from (a_fn.name.out)
+ end
+
+ new_json_parser (a_string: STRING): JSON_PARSER
+ do
+ create Result.make_with_string (a_string)
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+
+end
diff --git a/library/app_env/src/configuration/database_configuration.e b/library/app_env/src/configuration/database_configuration.e
new file mode 100644
index 0000000..3d28d5b
--- /dev/null
+++ b/library/app_env/src/configuration/database_configuration.e
@@ -0,0 +1,97 @@
+note
+ description: "Object that represent Database configuration settings"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ DATABASE_CONFIGURATION
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_driver: READABLE_STRING_32; a_connection: READABLE_STRING_32)
+ -- Create a database configuration setting: `driver' with `a_driver',
+ -- `database_string' with `a_connection'.
+ do
+ driver := a_driver
+ database_string := a_connection
+ ensure
+ driver_set: driver = a_driver
+ server_set: database_string = a_connection
+ end
+
+feature -- Access
+
+ driver: READABLE_STRING_32
+ --Database driver.
+
+ database_string: READABLE_STRING_32
+ -- Database connection.
+
+ connection_string: READABLE_STRING_32
+ -- Connection string
+ do
+ Result := {STRING_32} "Driver={" + driver + {STRING_32} "};" + database_string
+ end
+
+ item (a_param: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
+ local
+ s: READABLE_STRING_32
+ lower_s: READABLE_STRING_32
+ i,j: INTEGER
+ k: STRING_32
+ do
+ create k.make_from_string_general (a_param)
+ k.to_lower
+
+ s := database_string
+ lower_s := s.as_lower
+ i := lower_s.substring_index (k + {STRING_32} "=", 1)
+ if i > 0 then
+ if i = 1 or else s[i-1] = ';' then
+ j := s.index_of (';', i + k.count + 1)
+ if j = 0 then
+ j := s.count + 1
+ end
+ Result := s.substring (i + k.count + 1, j - 1)
+ end
+ end
+ end
+
+ server_name: detachable READABLE_STRING_32
+ do
+ Result := item ("Server")
+ end
+
+ port: INTEGER
+ do
+ if
+ attached item ("Port") as l_port and then
+ l_port.is_integer
+ then
+ Result := l_port.to_integer
+ end
+ end
+
+ database_name: detachable READABLE_STRING_32
+ do
+ Result := item ("Database")
+ end
+
+ user_id: detachable READABLE_STRING_32
+ do
+ Result := item ("Uid")
+ end
+
+ user_password: detachable READABLE_STRING_32
+ do
+ Result := item ("Pwd")
+ end
+
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/configuration/logger_configuration.e b/library/app_env/src/configuration/logger_configuration.e
new file mode 100644
index 0000000..d66e89c
--- /dev/null
+++ b/library/app_env/src/configuration/logger_configuration.e
@@ -0,0 +1,115 @@
+note
+ description: "Object that represents Logger configuration settings"
+ date: "$Date: 2015-02-03 19:11:23 +0100 (mar., 03 févr. 2015) $"
+ revision: "$Revision: 96575 $"
+
+class
+ LOGGER_CONFIGURATION
+
+inherit
+ LOG_PRIORITY_CONSTANTS
+ rename
+ default_create as log_default_create
+ end
+ ANY
+ redefine
+ default_create
+ select
+ default_create
+ end
+
+create
+ default_create
+
+feature -- Initialization
+
+ default_create
+ -- Initialize a Logger Instance configuration with default values
+ -- backups = `4' and level = `DEBUG'.
+ do
+ backup_count := 4
+ level := Log_debug
+ location := Void
+ type := {STRING_32} "null"
+ ensure then
+ backup_count_set: backup_count = 4
+ level_set: level = Log_debug
+ end
+
+feature -- Access
+
+ location: detachable PATH
+ -- Location for logs files, if none use default logs location.
+
+ backup_count: NATURAL
+ -- Max number of backup files.
+ -- When 0, no backup files are created, and the log file is simply truncated when it becomes larger than `max_file_size'.
+ -- When non-zero, the value specifies the maximum number of backup files.
+
+ level: INTEGER
+ -- Logger level.
+
+ type: IMMUTABLE_STRING_32
+ -- Type of logging.
+
+feature -- Element Change
+
+ set_location (a_location: detachable PATH)
+ -- Set `location' to `a_location'.
+ do
+ location := a_location
+ ensure
+ location_set: a_location ~ location
+ end
+
+ set_location_with_string (a_location: READABLE_STRING_GENERAL)
+ require
+ a_location /= Void and then not a_location.is_whitespace
+ do
+ set_location (create {PATH}.make_from_string (a_location))
+ end
+
+ set_type_with_string (a_type: detachable READABLE_STRING_GENERAL)
+ do
+ if a_type /= Void and then not a_type.is_whitespace then
+ create type.make_from_string_general (a_type)
+ else
+ create type.make_from_string_general ("null")
+ end
+ end
+
+ set_backup_count (a_backup: NATURAL)
+ -- Set backup_count to `a_backup'.
+ do
+ backup_count := a_backup
+ ensure
+ backup_count_set: backup_count = a_backup
+ end
+
+ set_level (a_level: READABLE_STRING_GENERAL)
+ -- Set a level based on `a_level'.
+ do
+ if a_level.is_case_insensitive_equal (emerg_str) then
+ level := log_emergency
+ elseif a_level.is_case_insensitive_equal (alert_str) then
+ level := log_alert
+ elseif a_level.is_case_insensitive_equal (crit_str) then
+ level := log_critical
+ elseif a_level.is_case_insensitive_equal (error_str) then
+ level := log_error
+ elseif a_level.is_case_insensitive_equal (warn_str) then
+ level := log_warning
+ elseif a_level.is_case_insensitive_equal (notic_str) then
+ level := log_notice
+ elseif a_level.is_case_insensitive_equal (info_str) then
+ level := log_information
+ elseif a_level.is_case_insensitive_equal (debug_str) then
+ level := log_debug
+ else
+ level := 0
+ end
+ end
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/error/basic_error_handler.e b/library/app_env/src/error/basic_error_handler.e
new file mode 100644
index 0000000..f010565
--- /dev/null
+++ b/library/app_env/src/error/basic_error_handler.e
@@ -0,0 +1,54 @@
+note
+ description: "Object handling error information"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ BASIC_ERROR_HANDLER
+
+create
+ make
+
+feature -- Initialization
+
+ make (a_error_message: READABLE_STRING_32; a_error_location: READABLE_STRING_32)
+ -- Create an object error, set `error_message' to `a_error_message'
+ -- set `error_location' to `a_error_location'.
+ do
+ set_error_message (a_error_message)
+ set_error_location (a_error_location)
+ ensure
+ error_message_set: error_message = a_error_message
+ error_location_set: error_location = a_error_location
+ end
+
+feature -- Access
+
+ error_message: READABLE_STRING_32
+ -- Message.
+
+ error_location: READABLE_STRING_32
+ -- Code to represent an error.
+
+feature -- Change Element
+
+ set_error_message (a_message: like error_message)
+ -- Set error_message with `a_message'.
+ do
+ error_message := a_message
+ ensure
+ message_set: error_message = a_message
+ end
+
+ set_error_location (a_location: READABLE_STRING_32)
+ -- Set error_location with `a_location'.
+ do
+ error_location := a_location
+ ensure
+ error_location_set: error_location = a_location
+ end
+
+note
+ copyright: "2011-2014, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/error/shared_error.e b/library/app_env/src/error/shared_error.e
new file mode 100644
index 0000000..1842ac8
--- /dev/null
+++ b/library/app_env/src/error/shared_error.e
@@ -0,0 +1,108 @@
+note
+ description: "Provides error information"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ SHARED_ERROR
+
+inherit
+
+ SHARED_LOGGER
+
+feature -- Access
+
+ last_error: detachable BASIC_ERROR_HANDLER
+ -- Object represent last error.
+
+ last_error_message: READABLE_STRING_32
+ -- Last error string representation.
+ do
+ if attached last_error as ll_error then
+ Result := ll_error.error_message
+ else
+ Result := ""
+ end
+ end
+
+feature -- Status Report
+
+ successful: BOOLEAN
+ -- Was last operation successful?
+ -- If not, `last_error' must be set.
+
+feature -- Element Settings
+
+ set_last_error_from_exception (a_location: STRING)
+ -- Initialize instance from last exception.
+ -- Don't show too much internal details (e.g. stack trace).
+ -- We really don't want this to fail since it is called from rescue clauses.
+ require
+ attached_location: a_location /= Void
+ local
+ l_exceptions: EXCEPTIONS
+ l_message: STRING
+ l_tag: detachable STRING
+ l_retried: BOOLEAN
+ do
+ if not l_retried then
+ create l_exceptions
+ create l_message.make (256)
+ l_tag := l_exceptions.tag_name
+ if l_tag /= Void then
+ l_message.append ("The following exception was raised: ")
+ l_message.append (l_tag)
+ else
+ l_message.append ("An unknown exception was raised.")
+ end
+ set_last_error (l_message, a_location)
+ write_critical_log (generator + ".set_last_error_from_exception " + l_message)
+ else
+ set_last_error ("Generic error", "")
+ write_critical_log (generator + ".set_last_error_from_exception Generic Error")
+ end
+ rescue
+ l_retried := True
+ retry
+ end
+
+ set_last_error (a_message, a_location: STRING)
+ -- Set `last_error_message' with `a_message',
+ -- `last_error_location' with `a_location' and
+ -- `successful' to `False'.
+ require
+ attached_message: a_message /= Void
+ attached_location: a_location /= Void
+ do
+ create last_error.make (a_message, a_location)
+ write_critical_log (generator + ".set_last_error " + a_message)
+ successful := False
+ ensure
+ last_error_set: attached last_error
+ failed: not successful
+ end
+
+ set_last_error_from_handler (a_error: detachable BASIC_ERROR_HANDLER)
+ -- Set `last_error' with `a_error'.
+ do
+ last_error := a_error
+ successful := False
+ ensure
+ last_error_set: attached last_error
+ failed: not successful
+ end
+
+ set_successful
+ -- Reset `last_error_message' and `last_error_location' and
+ -- set `successful' to `True'.
+ do
+ last_error := Void
+ successful := True
+ ensure
+ last_error__reset: last_error = Void
+ successful: successful
+ end
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/logger/logger.e b/library/app_env/src/logger/logger.e
new file mode 100644
index 0000000..2fc0857
--- /dev/null
+++ b/library/app_env/src/logger/logger.e
@@ -0,0 +1,223 @@
+note
+ description: "Object to log messages for a specific application. "
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ LOGGER
+
+inherit
+ ANY
+
+ LOG_PRIORITY_CONSTANTS
+ export
+ {NONE} all
+ end
+
+ SHARED_EXECUTION_ENVIRONMENT
+ export
+ {NONE} all
+ end
+
+create
+ make,
+ make_with_environment,
+ make_with_layout
+
+feature {NONE} -- Initialization
+
+ make
+ -- Initialize a logger object.
+ do
+ create log.make
+ end
+
+ make_with_environment (app: separate APPLICATION_ENVIRONMENT)
+ -- Initialize a logger object with an application environment `app'.
+ do
+ make
+ apply_environment (app)
+ end
+
+ make_with_layout (app: APPLICATION_ENVIRONMENT)
+ -- Initialize a logger object with an application layout `app'.
+ obsolete
+ "Use make_with_environment"
+ do
+ make_with_environment (app)
+ end
+
+feature -- Change
+
+ apply_environment (app: separate APPLICATION_ENVIRONMENT)
+ do
+ initialize_logger (app, log)
+ end
+
+feature {NONE} -- Internal
+
+ log: LOGGING_FACILITY
+
+feature -- Settings
+
+ level: INTEGER
+
+feature -- Logging
+
+ put_debug (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at debug level.
+ do
+ if level >= log_debug then
+ log.write_debug (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+ put_information (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at information level.
+ do
+ if level >= log_information then
+ log.write_information (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+ put_warning (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at warning level.
+ do
+ if level >= log_warning then
+ log.write_warning (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+ put_error (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at error level.
+ do
+ if level >= log_error then
+ log.write_error (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+ put_critical (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at critical level.
+ do
+ if level >= log_critical then
+ log.write_critical (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+ put_alert (a_message: separate READABLE_STRING_8)
+ -- Put message `a_message' to the log at alert level.
+ do
+ if level >= log_alert then
+ log.write_alert (create {STRING}.make_from_separate (a_message))
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ initialize_logger (app: separate APPLICATION_ENVIRONMENT; a_log: like log)
+ local
+ l_log_writer_file: LOG_ROLLING_WRITER_FILE
+ l_log_writer: detachable LOG_WRITER
+ l_logs_path: detachable PATH
+ l_logger_config: LOGGER_CONFIGURATION
+ ut: FILE_UTILITIES
+ p: PATH
+ l_name: IMMUTABLE_STRING_32
+ do
+ create l_name.make_from_separate (app.name)
+ create p.make_from_separate (app.application_config_path)
+-- l_name := app.name
+-- p := app.application_config_path
+
+ l_logger_config := new_logger_level_configuration (p)
+ if l_logger_config.type.is_case_insensitive_equal_general ("file") then
+ l_logs_path := l_logger_config.location
+ if l_logs_path = Void then
+ create l_logs_path.make_from_separate (app.logs_path)
+ end
+ if ut.directory_path_exists (l_logs_path) then
+ create l_log_writer_file.make_at_location (l_logs_path.extended (l_name).appended_with_extension ("log"))
+ l_log_writer_file.set_max_file_size ({NATURAL_64} 1024 * 1204)
+ l_log_writer_file.set_max_backup_count (l_logger_config.backup_count)
+ l_log_writer := l_log_writer_file
+ else
+ -- Should we create the directory anyway ?
+ end
+ elseif l_logger_config.type.is_case_insensitive_equal_general ("stderr") then
+ create {LOG_WRITER_STDERR} l_log_writer
+ end
+ if l_log_writer = Void then
+ create {LOG_WRITER_NULL} l_log_writer
+ set_logger_level (l_log_writer, log_notice)
+ else
+ set_logger_level (l_log_writer, 0) -- None
+ end
+ a_log.register_log_writer (l_log_writer)
+ end
+
+ set_logger_level (a_log_writer: LOG_WRITER; a_priority: INTEGER)
+ -- Setup the logger level based on `a_priority'
+ do
+ level := a_priority
+ if a_priority = log_debug then
+ a_log_writer.enable_debug_log_level
+ elseif a_priority = Log_emergency then
+ a_log_writer.enable_emergency_log_level
+ elseif a_priority = Log_alert then
+ a_log_writer.enable_alert_log_level
+ elseif a_priority = Log_critical then
+ a_log_writer.enable_critical_log_level
+ elseif a_priority = Log_error then
+ a_log_writer.enable_error_log_level
+ elseif a_priority = Log_warning then
+ a_log_writer.enable_warning_log_level
+ elseif a_priority = Log_notice then
+ a_log_writer.enable_notice_log_level
+ elseif a_priority = Log_information then
+ a_log_writer.enable_information_log_level
+ else
+ a_log_writer.enable_unkno_log_level
+ end
+ end
+
+ new_logger_level_configuration (a_path: PATH): LOGGER_CONFIGURATION
+ -- Retrieve a new logger level configuration.
+ -- By default, level is set to `DEBUG'.
+ local
+ l_parser: JSON_PARSER
+ ut: FILE_UTILITIES
+ do
+ create Result
+ if
+ ut.file_path_exists (a_path) and then
+ attached (create {JSON_FILE_READER}).read_json_from (a_path.name) as json_file
+ then
+ create l_parser.make_with_string (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached l_parser.parsed_json_object as jv and then
+ attached {JSON_OBJECT} jv.item ("logger") as l_logger
+ then
+ if attached {JSON_STRING} l_logger.item ("type") as l_type then
+ Result.set_type_with_string (l_type.item)
+ end
+ if attached {JSON_STRING} l_logger.item ("location") as l_location then
+ Result.set_location_with_string (l_location.item)
+ end
+ if attached {JSON_STRING} l_logger.item ("backup_count") as l_count then
+ if l_count.item.is_natural then
+ Result.set_backup_count (l_count.item.to_natural)
+ end
+ end
+ if attached {JSON_STRING} l_logger.item ("level") as l_level then
+ Result.set_level (l_level.item)
+ end
+ end
+ end
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/logger/logging_facility.e b/library/app_env/src/logger/logging_facility.e
new file mode 100644
index 0000000..00aa5b0
--- /dev/null
+++ b/library/app_env/src/logger/logging_facility.e
@@ -0,0 +1,139 @@
+note
+ description: "Wrapper class providing synchronize logging access."
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ LOGGING_FACILITY
+
+create
+ make
+
+feature -- Initialization
+
+ make
+ do
+ create logging.make
+ end
+
+feature -- Access
+
+ logging: LOG_LOGGING_FACILITY
+
+
+ register_log_writer (a_log_writer: LOG_WRITER)
+ -- Register the non-default log writer `a_log_writer'.
+ do
+ logging.register_log_writer (a_log_writer)
+ end
+
+feature -- Output
+
+
+ write_alert (msg: STRING)
+ -- Write `msg' to the log writers as an alert.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_alert (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_critical (msg: STRING)
+ -- Write `msg' to the log writers as an critical
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_critical (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_debug (msg: STRING)
+ -- Write `msg' to the log writers as an debug.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_debug (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_emergency (msg: STRING)
+ -- Write `msg' to the log writers as an emergency.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_emergency (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_error (msg: STRING)
+ -- Write `msg' to the log writers as an error.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_error (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_information (msg: STRING)
+ -- Write `msg' to the log writers as an information.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_information (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_notice (msg: STRING)
+ -- Write `msg' to the log writers as an notice.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_notice (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ write_warning (msg: STRING)
+ -- Write `msg' to the log writers as an warning.
+ local
+ l_retry: BOOLEAN
+ do
+ if not l_retry then
+ logging.write_warning (msg)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/app_env/src/logger/shared_logger.e b/library/app_env/src/logger/shared_logger.e
new file mode 100644
index 0000000..625f07d
--- /dev/null
+++ b/library/app_env/src/logger/shared_logger.e
@@ -0,0 +1,157 @@
+note
+ description: "Provides logger information"
+ date: "$Date: 2015-02-05 10:25:53 +0100 (jeu., 05 févr. 2015) $"
+ revision: "$Revision: 96584 $"
+
+class
+ SHARED_LOGGER
+
+inherit
+ LOG_PRIORITY_CONSTANTS
+
+ SHARED_EXECUTION_ENVIRONMENT
+
+feature -- Logger
+
+ logger: separate LOGGER
+ -- `log' facility (once per process)
+ -- that could be shared between threads
+ -- without reinitializing it.
+ do
+ Result := logger_cell_item (logger_cell)
+ end
+
+ logger_cell: separate CELL [separate LOGGER]
+ once ("PROCESS")
+ create Result.put (create {separate LOGGER}.make)
+ end
+
+ logger_cell_item (a_cell: like logger_cell): separate LOGGER
+ do
+ Result := a_cell.item
+ end
+
+feature -- Logging
+
+ write_debug_log (m: READABLE_STRING_8)
+ do
+-- write_debug_log_to (m, logger)
+ end
+
+ write_information_log (m: READABLE_STRING_8)
+ do
+-- write_information_log_to (m, logger)
+ end
+
+ write_warning_log (m: READABLE_STRING_8)
+ do
+-- write_warning_log_to (m, logger)
+ end
+
+ write_error_log (m: READABLE_STRING_8)
+ do
+-- write_error_log_to (m, logger)
+ end
+
+ write_critical_log (m: READABLE_STRING_8)
+ do
+-- write_critical_log_to (m, logger)
+ end
+
+ write_alert_log (m: READABLE_STRING_8)
+ do
+-- write_alert_log_to (m, logger)
+ end
+
+feature {NONE} -- Logger: separate implementation
+
+ write_debug_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_debug (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+ write_information_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_information (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+ write_warning_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_warning (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+ write_error_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_error (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+ write_critical_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_critical (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+ write_alert_log_to (m: READABLE_STRING_8; a_log: like logger)
+ local
+ retried: BOOLEAN
+ do
+ if not retried then
+ a_log.put_alert (m)
+ end
+ rescue
+ retried := True
+ retry
+ end
+
+feature {NONE} -- Implementation
+
+ initialize_logger (app: APPLICATION_ENVIRONMENT)
+ local
+ l_logger: separate LOGGER
+ do
+ create l_logger.make_with_environment (app)
+ set_logger_to (l_logger, logger_cell)
+ end
+
+ set_logger_to (a_logger: separate LOGGER; a_cell: like logger_cell)
+ do
+ a_cell.replace (a_logger)
+ end
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/configuration/config-safe.ecf b/library/configuration/config-safe.ecf
new file mode 100644
index 0000000..b29475b
--- /dev/null
+++ b/library/configuration/config-safe.ecf
@@ -0,0 +1,16 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
diff --git a/library/configuration/config.ecf b/library/configuration/config.ecf
new file mode 100644
index 0000000..9be28f0
--- /dev/null
+++ b/library/configuration/config.ecf
@@ -0,0 +1,16 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
diff --git a/library/configuration/license.lic b/library/configuration/license.lic
new file mode 100644
index 0000000..8ad712a
--- /dev/null
+++ b/library/configuration/license.lic
@@ -0,0 +1,10 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR}, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
diff --git a/library/configuration/src/config_reader.e b/library/configuration/src/config_reader.e
new file mode 100644
index 0000000..2698b35
--- /dev/null
+++ b/library/configuration/src/config_reader.e
@@ -0,0 +1,163 @@
+note
+ description: "Summary description for {CONFIG_READER}."
+ author: ""
+ date: "$Date: 2014-12-18 16:37:11 +0100 (jeu., 18 déc. 2014) $"
+ revision: "$Revision: 96383 $"
+
+deferred class
+ CONFIG_READER
+
+inherit
+ SHARED_EXECUTION_ENVIRONMENT
+
+feature -- Status report
+
+ has_item (k: READABLE_STRING_GENERAL): BOOLEAN
+ -- Has item associated with key `k'?
+ deferred
+ end
+
+ has_error: BOOLEAN
+ -- Has error?
+ --| Syntax error, source not found, ...
+ deferred
+ end
+
+feature -- Query
+
+ resolved_text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
+ -- String item associated with key `k' and expanded to resolved variables ${varname}.
+ do
+ if attached text_item (k) as s then
+ Result := resolved_expression (s)
+ end
+ end
+
+ resolved_text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32]
+ -- List of String item associated with key `k',
+ -- and expanded values to resolved variables ${varname}.
+ do
+ if attached text_list_item (k) as lst then
+ from
+ lst.start
+ until
+ lst.after
+ loop
+ lst.replace (resolved_expression (lst.item))
+ lst.forth
+ end
+ end
+ end
+
+ resolved_text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32]
+ -- Table of String item associated with key `k',
+ -- and expanded values to resolved variables ${varname}.
+ do
+ if attached text_table_item (k) as tb then
+ from
+ tb.start
+ until
+ tb.after
+ loop
+ tb.replace (resolved_expression (tb.item_for_iteration), tb.key_for_iteration)
+ tb.forth
+ end
+ end
+ end
+
+ text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
+ -- String item associated with key `k'.
+ deferred
+ end
+
+ text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32]
+ -- List of String item associated with key `k'.
+ deferred
+ end
+
+ text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32]
+ -- Table of String item associated with key `k'.
+ deferred
+ end
+
+ integer_item (k: READABLE_STRING_GENERAL): INTEGER
+ -- Integer item associated with key `k'.
+ deferred
+ ensure
+ not has_item (k) implies Result = 0
+ end
+
+feature {NONE} -- Implementation
+
+ resolved_items: detachable STRING_TABLE [READABLE_STRING_32]
+ -- Resolved items indexed by expression.
+
+ resolved_expression (exp: READABLE_STRING_GENERAL): STRING_32
+ -- Resolved `exp' using `Current' or else environment variables.
+ local
+ i,n,b,e: INTEGER
+ k: detachable READABLE_STRING_GENERAL
+ c: CHARACTER_32
+ l_resolved_items: like resolved_items
+ l_text: detachable READABLE_STRING_GENERAL
+ do
+ from
+ i := 1
+ n := exp.count
+ create Result.make (n)
+ until
+ i > n
+ loop
+ c := exp[i]
+ if i + 1 < n and then c = '$' and then exp[i+1] = '{' then
+ b := i + 2
+ e := exp.index_of ('}', b) - 1
+ if e > 0 then
+ k := exp.substring (b, e)
+ l_text := Void
+ l_resolved_items := resolved_items
+ if
+ l_resolved_items /= Void and then
+ attached l_resolved_items.item (k) as s
+ then
+ -- Already resolved, reuse value.
+ l_text := s
+ elseif attached text_item (k) as s then
+ -- has such item
+ l_text := s
+ elseif attached execution_environment.item (k) as v then
+ -- Or from environment
+ l_text := v
+ else
+ l_text := exp.substring (i, e + 1)
+ end
+ i := e + 1
+ Result.append_string_general (l_text)
+ else
+ Result.extend (c)
+ end
+ else
+ Result.extend (c)
+ end
+ i := i + 1
+ end
+ end
+
+feature -- Duplication
+
+ sub_config (k: READABLE_STRING_GENERAL): detachable CONFIG_READER
+ -- Configuration representing a subset of Current for key `k'.
+ deferred
+ end
+
+note
+ copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/configuration/src/ini_config.e b/library/configuration/src/ini_config.e
new file mode 100644
index 0000000..0456e43
--- /dev/null
+++ b/library/configuration/src/ini_config.e
@@ -0,0 +1,534 @@
+note
+ description: "[
+ Object parsing a .ini file.
+
+ Lines starting by '#', or ';', or "--" are comments
+ Section are declared using brackets "[section_name]"
+ Values are declared as "key = value"
+ List values are declared as multiple lines "key[] = value"
+ example:
+ --------------------
+ [first_section]
+ one = abc
+
+ [second_section]
+ two = def
+ lst[] = a
+ lst[] = b
+ lst[] = c
+
+ [third_section]
+ three = ghi
+ table[a] = foo
+ table[b] = bar
+ --------------------
+
+ Values are accessible from `items' or by section from `sections'
+ `item' has smart support for '.'
+
+ item ("one") -> abc
+ item ("two") -> def
+ item ("second_section.two") -> def
+ item ("table[b]") -> foo
+ item ("table.b") -> foo
+ item ("third_section.table[b]") -> foo
+ item ("third_section.table.b") -> foo
+
+ notes:
+ it is considered that the .ini file is utf-8 encoded
+ keys are unicode string
+ values are stored UTF-8 string, but one can use unicode_string_item to convert to STRING_32
+
+ Additional features:
+ - Include other files:
+ @include=file-to-include
+
+ ]"
+ date: "$Date: 2015-03-09 19:25:49 +0100 (lun., 09 mars 2015) $"
+ revision: "$Revision: 96797 $"
+
+class
+ INI_CONFIG
+
+inherit
+ CONFIG_READER
+
+ TABLE_ITERABLE [ANY, READABLE_STRING_GENERAL]
+
+ SHARED_EXECUTION_ENVIRONMENT
+
+create
+ make_from_file,
+ make_from_string,
+ make_from_items
+
+feature {NONE} -- Initialization
+
+ make_from_file (p: PATH)
+ do
+ initialize
+ parse_file (p)
+ end
+
+ make_from_string (a_content: STRING_8)
+ do
+ initialize
+ parse_content (a_content)
+ end
+
+ make_from_items (a_items: like items)
+ do
+ initialize
+ across
+ a_items as ic
+ loop
+ items.force (ic.item, ic.key)
+ end
+ end
+
+ initialize
+ -- Initialize data.
+ do
+ associated_path := Void
+ create utf
+ create items.make (0)
+ create sections.make (0)
+ end
+
+ reset
+ -- Reset internal data.
+ do
+ has_error := False
+ items.wipe_out
+ sections.wipe_out
+ end
+
+feature -- Status report
+
+ has_item (k: READABLE_STRING_GENERAL): BOOLEAN
+ -- Has item associated with key `k'?
+ do
+ Result := item (k) /= Void
+ end
+
+ has_error: BOOLEAN
+ -- Has error?
+ --| Syntax error, source not found, ...
+
+feature -- Access: Config Reader
+
+ text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
+ -- String item associated with key `k'.
+ do
+ Result := value_to_string_32 (item (k))
+ end
+
+ text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32]
+ -- List of String item associated with key `k'.
+ do
+ if attached {LIST [READABLE_STRING_8]} item (k) as l_list then
+ create {ARRAYED_LIST [READABLE_STRING_32]} Result.make (l_list.count)
+ Result.compare_objects
+ across
+ l_list as ic
+ until
+ Result = Void
+ loop
+ if attached value_to_string_32 (ic.item) as s32 then
+ Result.force (s32)
+ else
+ Result := Void
+ end
+ end
+ end
+ end
+
+ text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32]
+ -- Table of String item associated with key `k'.
+ do
+ if attached {STRING_TABLE [READABLE_STRING_8]} item (k) as l_list then
+ create {STRING_TABLE [READABLE_STRING_32]} Result.make (l_list.count)
+ Result.compare_objects
+ across
+ l_list as ic
+ until
+ Result = Void
+ loop
+ if attached value_to_string_32 (ic.item) as s32 then
+ Result.force (s32, ic.key)
+ else
+ Result := Void
+ end
+ end
+ end
+ end
+
+ integer_item (k: READABLE_STRING_GENERAL): INTEGER
+ -- Integer item associated with key `k'.
+ do
+ if attached {READABLE_STRING_GENERAL} item (k) as s then
+ Result := s.to_integer
+ end
+ end
+
+ associated_path: detachable PATH
+ -- If current was built from a filename, return this path.
+
+feature -- Duplication
+
+ sub_config (k: READABLE_STRING_GENERAL): detachable CONFIG_READER
+ -- Configuration representing a subset of Current for key `k'.
+ do
+ if attached sections.item (k) as l_items then
+ create {INI_CONFIG} Result.make_from_items (l_items)
+ end
+ end
+
+feature -- Access
+
+ item (k: READABLE_STRING_GENERAL): detachable ANY
+ -- Value associated with key `k'.
+ local
+ i: INTEGER
+ s,sk: detachable READABLE_STRING_GENERAL
+ do
+ -- Try first directly in values
+ Result := items.item (k)
+ if Result = Void then
+ --| If there is a dot
+ --| this could be
+ --| section.variable
+ --| variable.name or variable[name]
+ i := k.index_of ('.', 1)
+ if i > 0 then
+ s := k.head (i - 1)
+ -- then search first in section
+ if attached sections.item (s) as l_section then
+ sk := k.substring (i + 1, k.count)
+ Result := l_section.item (sk)
+ if Result = Void then
+ Result := item_from_values (l_section, sk)
+ end
+ end
+ if Result = Void then
+ i := k.index_of ('.', i + 1)
+ if i > 0 then
+ -- There is another dot .. could be due to include
+ across
+ sections as ic
+ until
+ Result /= Void
+ loop
+ s := ic.key
+ -- If has other dot, this may be in sub sections ...
+ if s.has ('.') and then k.starts_with (s) and then k[s.count + 1] = '.' then
+ if attached sections.item (s) as l_section then
+ sk := k.substring (s.count + 2, k.count)
+ Result := l_section.item (sk)
+ if Result = Void then
+ Result := item_from_values (l_section, sk)
+ end
+ end
+ end
+ end
+ end
+ if Result = Void then
+ -- otherwise in values object.
+ Result := item_from_values (items, k)
+ end
+ end
+ else
+ --| Could be
+ --| variable[name]
+ Result := item_from_values (items, k)
+ end
+ end
+ end
+
+ items: STRING_TABLE [ANY]
+
+ sections: STRING_TABLE [like items]
+
+feature -- Access
+
+ new_cursor: TABLE_ITERATION_CURSOR [ANY, READABLE_STRING_GENERAL]
+ -- Fresh cursor associated with current structure
+ do
+ Result := items.new_cursor
+ end
+
+feature {NONE} -- Implementation
+
+ value_to_string_32 (obj: detachable ANY): detachable STRING_32
+ do
+ if attached {READABLE_STRING_32} obj as s32 then
+ Result := s32
+ elseif attached {READABLE_STRING_8} obj as s then
+ Result := utf.utf_8_string_8_to_escaped_string_32 (s)
+ end
+ end
+
+ item_from_values (a_values: STRING_TABLE [ANY]; k: READABLE_STRING_GENERAL): detachable ANY
+ local
+ i,j: INTEGER
+ s: READABLE_STRING_GENERAL
+ do
+ Result := a_values.item (k)
+ if Result = Void then
+ i := k.index_of ('.', 1)
+ if i > 0 then
+ s := k.head (i - 1)
+ if attached {STRING_TABLE [ANY]} a_values.item (s) as l_table then
+ Result := l_table.item (k.substring (i + 1, k.count))
+ end
+ else
+ i := k.index_of ('[', 1)
+ if i > 0 then
+ j := k.index_of (']', i + 1)
+ if j = k.count then
+ s := k.head (i - 1)
+ if attached {STRING_TABLE [ANY]} a_values.item (s) as l_table then
+ Result := l_table.item (k.substring (i + 1, j - 1))
+ end
+ end
+ end
+ end
+ end
+ end
+
+ record_in_section (obj: ANY; k: READABLE_STRING_GENERAL; a_section: READABLE_STRING_GENERAL)
+ local
+ v: detachable like items
+ do
+ v := sections.item (a_section)
+ if v = Void then
+ create v.make (1)
+ sections.force (v, a_section)
+ end
+ v.force (obj, k)
+ end
+
+ parse_content (a_content: STRING_8)
+ local
+ i,j,n: INTEGER
+ s: READABLE_STRING_8
+ do
+ last_section_name := Void
+ from
+ i := 1
+ n := a_content.count
+ until
+ i > n
+ loop
+ j := a_content.index_of ('%N', i)
+ if j > 0 then
+ s := a_content.substring (i, j - 1)
+ i := j + 1
+ if i <= n and then a_content[i] = '%R' then
+ i := i + 1
+ end
+ else
+ j := n
+ s := a_content.substring (i, j)
+ i := j + 1
+ end
+ analyze_line (s, Void)
+ variant
+ i
+ end
+ last_section_name := Void
+ rescue
+ last_section_name := Void
+ has_error := True
+ end
+
+ parse_file (p: PATH)
+ do
+ associated_path := p
+ last_section_name := Void
+ import_path (p, Void)
+ last_section_name := Void
+ end
+
+ import_path (p: PATH; a_section_prefix: detachable READABLE_STRING_32)
+ -- Import config from path `p'.
+ local
+ f: PLAIN_TEXT_FILE
+ l_last_section_name: like last_section_name
+ retried: BOOLEAN
+ l_old_associated_path: like associated_path
+ do
+ l_old_associated_path := associated_path
+ if retried then
+ has_error := True
+ else
+ associated_path := p
+ l_last_section_name := last_section_name
+ last_section_name := Void
+ create f.make_with_path (p)
+ if f.exists and then f.is_access_readable then
+ f.open_read
+ from
+ until
+ f.exhausted or f.end_of_file
+ loop
+ f.read_line_thread_aware
+ analyze_line (f.last_string, a_section_prefix)
+ end
+ f.close
+ else
+ -- File not readable
+ has_error := True
+ end
+ end
+ last_section_name := l_last_section_name
+ associated_path := l_old_associated_path
+ rescue
+ retried := True
+ retry
+ end
+
+ analyze_line (a_line: STRING_8; a_section_prefix: detachable READABLE_STRING_32)
+ -- Analyze line `a_line'.
+ local
+ k,sk: STRING_32
+ v: STRING_8
+ obj: detachable ANY
+ lst: LIST [STRING_8]
+ tb: STRING_TABLE [STRING_8]
+ i,j: INTEGER
+ p: PATH
+ l_section_name: like last_section_name
+ do
+ obj := Void
+ a_line.left_adjust
+ if
+ a_line.is_empty
+ or a_line.is_whitespace
+ or a_line.starts_with_general ("#")
+ or a_line.starts_with_general (";")
+ or a_line.starts_with_general ("--")
+ then
+ -- Ignore
+ elseif a_line.starts_with_general ("[") then
+ i := a_line.index_of (']', 1)
+ l_section_name := utf.utf_8_string_8_to_string_32 (a_line.substring (2, i - 1))
+ l_section_name.left_adjust
+ l_section_name.right_adjust
+ if a_section_prefix /= Void then
+ l_section_name.prepend_character ('.')
+ l_section_name.prepend (a_section_prefix)
+ end
+ last_section_name := l_section_name
+ else
+ i := a_line.index_of ('=', 1)
+ if i > 1 then
+ k := utf.utf_8_string_8_to_string_32 (a_line.head (i - 1))
+ k.right_adjust
+ v := a_line.substring (i + 1, a_line.count)
+ v.left_adjust
+ v.right_adjust
+
+
+ if k.is_case_insensitive_equal_general ("@include") then
+ create p.make_from_string (v)
+ if not p.is_absolute and attached associated_path as l_path then
+ p := l_path.parent.extended_path (p)
+ end
+ import_path (p, last_section_name)
+ else
+ i := k.index_of ('[', 1)
+ if i > 0 then
+ j := k.index_of (']', i + 1)
+ if j = i + 1 then -- ends_with "[]"
+ k.keep_head (i - 1)
+ if
+ a_section_prefix /= Void and then
+ attached {LIST [STRING_8]} items.item (a_section_prefix + {STRING_32} "." + k) as l_list
+ then
+ lst := l_list
+ elseif
+ attached last_section_name as l_section_prefix and then
+ attached {LIST [STRING_8]} items.item (l_section_prefix + {STRING_32} "." + k) as l_list
+ then
+ lst := l_list
+ elseif attached {LIST [STRING_8]} items.item (k) as l_list then
+ lst := l_list
+ else
+ create {ARRAYED_LIST [STRING_8]} lst.make (1)
+ record_item (lst, k, a_section_prefix)
+ end
+ lst.force (v)
+ obj := lst
+ elseif j > i then
+ -- table
+ sk := k.substring (i + 1, j - 1)
+ sk.left_adjust
+ sk.right_adjust
+ k.keep_head (i - 1)
+ if
+ a_section_prefix /= Void and then
+ attached {STRING_TABLE [STRING_8]} items.item (a_section_prefix + {STRING_32} "." + k) as l_table
+ then
+ tb := l_table
+ elseif
+ attached last_section_name as l_section_prefix and then
+ attached {STRING_TABLE [STRING_8]} items.item (l_section_prefix + {STRING_32} "." + k) as l_table
+ then
+ tb := l_table
+ elseif attached {STRING_TABLE [STRING_8]} items.item (k) as l_table then
+ tb := l_table
+ else
+ create tb.make (1)
+ record_item (tb, k, a_section_prefix)
+ end
+ tb.force (v, sk)
+ obj := tb
+ else
+ -- Error missing closing ']'
+ has_error := True
+ end
+ else
+ record_item (v, k, a_section_prefix)
+ obj := v
+ end
+
+ if attached last_section_name as l_session and then obj /= Void then
+ record_in_section (obj, k, l_session)
+ end
+ end
+ else
+ -- Error
+ has_error := True
+ end
+ end
+ end
+
+ record_item (v: ANY; k: READABLE_STRING_32; a_section_prefix: detachable READABLE_STRING_32)
+ do
+ if a_section_prefix /= Void then
+ items.force (v, a_section_prefix + {STRING_32} "." + k)
+ else
+ items.force (v, k)
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ last_section_name: detachable STRING_32
+
+ utf: UTF_CONVERTER
+
+invariant
+
+note
+ copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/configuration/src/json_config.e b/library/configuration/src/json_config.e
new file mode 100644
index 0000000..54fe3d3
--- /dev/null
+++ b/library/configuration/src/json_config.e
@@ -0,0 +1,220 @@
+note
+ description: "Object parsing a JSON configuration file."
+ date: "$Date: 2014-12-18 16:37:11 +0100 (jeu., 18 déc. 2014) $"
+ revision: "$Revision: 96383 $"
+
+class
+ JSON_CONFIG
+
+inherit
+ CONFIG_READER
+
+create
+ make_from_file,
+ make_from_string,
+ make_from_json_object
+
+feature {NONE} -- Initialization
+
+ make_from_file (p: PATH)
+ -- Create object from a file `p'
+ do
+ parse (p)
+ end
+
+ make_from_string (a_json: READABLE_STRING_8)
+ -- Create current config from string `a_json'.
+ local
+ l_parser: JSON_PARSER
+ do
+ create l_parser.make_with_string (a_json)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached l_parser.parsed_json_object as j_o
+ then
+ make_from_json_object (j_o)
+ else
+ has_error := True
+ end
+ end
+
+ make_from_json_object (a_object: JSON_OBJECT)
+ -- Create current config from JSON `a_object'.
+ do
+ associated_path := Void
+ json_value := a_object
+ end
+
+feature -- Status report
+
+ has_item (k: READABLE_STRING_GENERAL): BOOLEAN
+ -- Has item associated with key `k'?
+ do
+ Result := item (k) /= Void
+ end
+
+ has_error: BOOLEAN
+ -- Has error?
+ --| Syntax error, source not found, ...
+
+feature -- Access: Config Reader
+
+ text_item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
+ -- String item associated with query `k'.
+ do
+ Result := value_to_string_32 (item (k))
+ end
+
+ text_list_item (k: READABLE_STRING_GENERAL): detachable LIST [READABLE_STRING_32]
+ -- List of String item associated with key `k'.
+ do
+ if attached {JSON_ARRAY} item (k) as l_array then
+ create {ARRAYED_LIST [READABLE_STRING_32]} Result.make (l_array.count)
+ Result.compare_objects
+ across
+ l_array as ic
+ until
+ Result = Void
+ loop
+ if attached value_to_string_32 (ic.item) as s32 then
+ Result.force (s32)
+ else
+ Result := Void
+ end
+ end
+ end
+ end
+
+ text_table_item (k: READABLE_STRING_GENERAL): detachable STRING_TABLE [READABLE_STRING_32]
+ -- Table of String item associated with key `k'.
+ do
+ if attached {JSON_OBJECT} item (k) as obj then
+ create {STRING_TABLE [READABLE_STRING_32]} Result.make (obj.count)
+ Result.compare_objects
+ across
+ obj as ic
+ until
+ Result = Void
+ loop
+ if attached value_to_string_32 (ic.item) as s32 then
+ Result.force (s32, ic.key.item)
+ else
+ Result := Void
+ end
+ end
+ end
+ end
+
+ integer_item (k: READABLE_STRING_GENERAL): INTEGER
+ -- Integer item associated with key `k'.
+ do
+ if attached {JSON_NUMBER} item (k) as l_number then
+ Result := l_number.item.to_integer
+ end
+ end
+
+ associated_path: detachable PATH
+ -- If current was built from a filename, return this path.
+
+feature -- Duplication
+
+ sub_config (k: READABLE_STRING_GENERAL): detachable CONFIG_READER
+ -- Configuration representing a subset of Current for key `k'.
+ do
+ if attached {JSON_OBJECT} item (k) as j_o then
+ create {JSON_CONFIG} Result.make_from_json_object (j_o)
+ end
+ end
+
+feature -- Access
+
+ item (k: READABLE_STRING_GENERAL): detachable JSON_VALUE
+ -- Item associated with query `k' if any.
+ -- `k' can be a single name such as "foo",
+ -- or a qualified name such as "foo.bar" (assuming that "foo" is associated with a JSON object).
+ do
+ if attached json_value as obj then
+ Result := object_json_value (obj, k.to_string_32)
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ value_to_string_32 (v: detachable ANY): detachable STRING_32
+ do
+ if attached {JSON_STRING} v as l_string then
+ Result := l_string.unescaped_string_32
+ elseif attached {JSON_NUMBER} v as l_number then
+ Result := l_number.item
+ end
+ end
+
+ object_json_value (a_object: JSON_OBJECT; a_query: READABLE_STRING_32): detachable JSON_VALUE
+ -- Item associated with query `a_query' from object `a_object' if any.
+ local
+ i: INTEGER
+ h: READABLE_STRING_32
+ do
+ i := a_query.index_of ('.', 1)
+ if i > 1 then
+ h := a_query.head (i - 1)
+ if attached {JSON_OBJECT} a_object.item (h) as l_obj then
+ Result := object_json_value (l_obj, a_query.substring (i + 1, a_query.count))
+ else
+ -- This is not an object...
+ Result := Void
+ end
+ else
+ Result := a_object.item (a_query)
+ end
+ end
+
+ parse (a_path: PATH)
+ local
+ l_parser: JSON_PARSER
+ do
+ associated_path := a_path
+ has_error := False
+ if attached json_file_from (a_path) as json_file then
+ l_parser := new_json_parser (json_file)
+ l_parser.parse_content
+ if
+ l_parser.is_valid and then
+ attached l_parser.parsed_json_object as jv
+ then
+ json_value := jv
+ else
+ has_error := True
+ end
+ else
+ has_error := True
+ end
+ end
+
+feature {NONE} -- JSON
+
+ json_value: detachable JSON_OBJECT
+ -- Possible json object representation.
+
+ json_file_from (a_fn: PATH): detachable STRING
+ do
+ Result := (create {JSON_FILE_READER}).read_json_from (a_fn.name.out)
+ end
+
+ new_json_parser (a_string: STRING): JSON_PARSER
+ do
+ create Result.make_with_string (a_string)
+ end
+
+note
+ copyright: "2011-2015, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/configuration/tests/config_tests-safe.ecf b/library/configuration/tests/config_tests-safe.ecf
new file mode 100644
index 0000000..351fb38
--- /dev/null
+++ b/library/configuration/tests/config_tests-safe.ecf
@@ -0,0 +1,18 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
diff --git a/library/configuration/tests/config_tests.ecf b/library/configuration/tests/config_tests.ecf
new file mode 100644
index 0000000..3f93907
--- /dev/null
+++ b/library/configuration/tests/config_tests.ecf
@@ -0,0 +1,18 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
diff --git a/library/configuration/tests/test_config_reader_set.e b/library/configuration/tests/test_config_reader_set.e
new file mode 100644
index 0000000..2f90530
--- /dev/null
+++ b/library/configuration/tests/test_config_reader_set.e
@@ -0,0 +1,221 @@
+note
+ description: "[
+ Testing suite for CONFIG_READER .
+ ]"
+ author: "$Author: jfiat $"
+ date: "$Date: 2014-12-18 16:37:11 +0100 (jeu., 18 déc. 2014) $"
+ revision: "$Revision: 96383 $"
+
+class
+ TEST_CONFIG_READER_SET
+
+inherit
+ EQA_TEST_SET
+
+feature -- Test
+
+ test_ini
+ local
+ cfg: CONFIG_READER
+ do
+ create {INI_CONFIG} cfg.make_from_string ("[
+ foo = bar
+
+ collection[] = a
+ collection[] = b
+ collection[] = c
+ collection[] = 1
+ collection[] = 2
+ collection[] = 3
+
+ table[a] = 1
+ table[b] = 2
+ table[c] = 3
+ table[d] = test
+
+ [first]
+ abc = 1
+ def = and so on
+ ghi = "path"
+
+ [ second ]
+ this = 1
+ is = 2
+ the = 3
+ end = 4
+
+ ]")
+
+ assert ("is valid", not cfg.has_error)
+ assert ("has_item (foo)", cfg.has_item ("foo"))
+ assert ("has_item (abc)", cfg.has_item ("abc"))
+ assert ("has_item (def)", cfg.has_item ("def"))
+ assert ("has_item (ghi)", cfg.has_item ("ghi"))
+ assert ("has_item (this)", cfg.has_item ("this"))
+ assert ("has_item (is)", cfg.has_item ("is"))
+ assert ("has_item (the)", cfg.has_item ("the"))
+ assert ("has_item (end)", cfg.has_item ("end"))
+
+ assert ("item (foo)", attached cfg.text_item ("foo") as v and then v.same_string_general ("bar"))
+ assert ("item (abc)", attached cfg.text_item ("abc") as v and then v.same_string_general ("1"))
+ assert ("item (def)", attached cfg.text_item ("def") as v and then v.same_string_general ("and so on"))
+ assert ("item (ghi)", attached cfg.text_item ("ghi") as v and then v.same_string_general ("%"path%""))
+ assert ("item (this)", attached cfg.text_item ("this") as v and then v.same_string_general ("1"))
+ assert ("item (is)", attached cfg.text_item ("is") as v and then v.same_string_general ("2"))
+ assert ("item (the)", attached cfg.text_item ("the") as v and then v.same_string_general ("3"))
+ assert ("item (end)", attached cfg.text_item ("end") as v and then v.same_string_general ("4"))
+
+ assert ("has_item (first.abc)", cfg.has_item ("first.abc"))
+ assert ("item (first.abc)", attached cfg.text_item ("first.abc") as v and then v.same_string_general ("1"))
+ assert ("has_item (second.is)", cfg.has_item ("second.is"))
+ assert ("item (second.is)", attached cfg.text_item ("second.is") as v and then v.same_string_general ("2"))
+
+ assert ("has_item (collection)", cfg.has_item ("collection"))
+ assert ("item (collection)", attached cfg.text_list_item ("collection") as lst and then (
+ lst.has ("a") and lst.has ("b") and lst.has ("c") and lst.has ("1") and lst.has ("2") and lst.has ("3")
+ )
+ )
+
+ assert ("has_item (table)", cfg.has_item ("table"))
+ assert ("item (table)", attached cfg.text_table_item ("table") as tb and then (
+ tb.item ("a") ~ {STRING_32} "1" and
+ tb.item ("b") ~ {STRING_32} "2" and
+ tb.item ("c") ~ {STRING_32} "3" and
+ tb.item ("d") ~ {STRING_32} "test"
+ )
+ )
+
+ if attached cfg.sub_config ("second") as cfg_second then
+ assert ("has_item (is)", cfg_second.has_item ("is"))
+ assert ("item (is)", attached cfg_second.text_item ("is") as v and then v.same_string_general ("2"))
+
+ else
+ assert ("has second", False)
+ end
+
+ end
+
+ test_resolver_ini
+ local
+ cfg: CONFIG_READER
+ do
+ create {INI_CONFIG} cfg.make_from_string ("[
+ foo = bar
+
+ [extra]
+ a.b.c = abc
+
+ [expression]
+ text = ${foo}/${a.b.c}
+ ]")
+
+ assert ("is valid", not cfg.has_error)
+ assert ("has_item (extra.a.b.c)", cfg.has_item ("extra.a.b.c"))
+ assert ("has_item (extra.a.b.c)", cfg.has_item ("extra.a.b.c"))
+ assert ("has_item (expression.text)", cfg.has_item ("expression.text"))
+ assert ("item (expression.text)", attached cfg.resolved_text_item ("expression.text") as s and then s.same_string_general ("bar/abc"))
+ end
+
+ test_deep_ini
+ local
+ cfg: CONFIG_READER
+ f: RAW_FILE
+ do
+
+ create f.make_with_name ("test_deep.ini")
+ f.create_read_write
+ f.put_string ("[
+ test = extra
+ [new]
+ enabled = true
+ ]"
+ )
+ f.close
+ create {INI_CONFIG} cfg.make_from_string ("[
+ foo = bar
+
+ [extra]
+ a.b.c = abc
+
+ [outside]
+ before = include
+ @include=test_deep.ini
+
+ ]")
+ f.delete
+
+ assert ("is valid", not cfg.has_error)
+ assert ("has_item (extra.a.b.c)", cfg.has_item ("extra.a.b.c"))
+ assert ("has_item (extra.a.b.c)", cfg.has_item ("extra.a.b.c"))
+ assert ("has_item (outside.new.enabled)", cfg.has_item ("outside.new.enabled"))
+ end
+
+
+ test_json
+ local
+ cfg: CONFIG_READER
+ do
+ create {JSON_CONFIG} cfg.make_from_string ("[
+ {
+ "foo": "bar",
+ "first": {
+ "abc": 1,
+ "def": "and so on",
+ "ghi": "\"path\""
+ },
+ "second": {
+ "this": 1,
+ "is": 2,
+ "the": 3,
+ "end": 4
+ },
+ "collection": ["a", "b", "c", 1, 2, 3],
+ "table": { "a": 1, "b": 2, "c": 3, "d" : "test" }
+ }
+ ]")
+
+ assert ("is valid", not cfg.has_error)
+ assert ("has_item (foo)", cfg.has_item ("foo"))
+ assert ("has_item (first.abc)", cfg.has_item ("first.abc"))
+ assert ("has_item (first.def)", cfg.has_item ("first.def"))
+ assert ("has_item (first.ghi)", cfg.has_item ("first.ghi"))
+ assert ("has_item (second.this)", cfg.has_item ("second.this"))
+ assert ("has_item (second.is)", cfg.has_item ("second.is"))
+ assert ("has_item (second.the)", cfg.has_item ("second.the"))
+ assert ("has_item (second.end)", cfg.has_item ("second.end"))
+
+ assert ("item (foo)", attached cfg.text_item ("foo") as v and then v.same_string_general ("bar"))
+ assert ("item (first.abc)", attached cfg.text_item ("first.abc") as v and then v.same_string_general ("1"))
+ assert ("item (first.def)", attached cfg.text_item ("first.def") as v and then v.same_string_general ("and so on"))
+ assert ("item (first.ghi)", attached cfg.text_item ("first.ghi") as v and then v.same_string_general ("%"path%""))
+ assert ("item (second.this)", attached cfg.text_item ("second.this") as v and then v.same_string_general ("1"))
+ assert ("item (second.is)", attached cfg.text_item ("second.is") as v and then v.same_string_general ("2"))
+ assert ("item (second.the)", attached cfg.text_item ("second.the") as v and then v.same_string_general ("3"))
+ assert ("item (second.end)", attached cfg.text_item ("second.end") as v and then v.same_string_general ("4"))
+
+ assert ("has_item (collection)", cfg.has_item ("collection"))
+ assert ("item (collection)", attached cfg.text_list_item ("collection") as lst and then (
+ lst.has ("a") and lst.has ("b") and lst.has ("c") and lst.has ("1") and lst.has ("2") and lst.has ("3")
+ )
+ )
+
+ assert ("has_item (table)", cfg.has_item ("table"))
+ assert ("item (table)", attached cfg.text_table_item ("table") as tb and then (
+ tb.item ("a") ~ {STRING_32} "1" and
+ tb.item ("b") ~ {STRING_32} "2" and
+ tb.item ("c") ~ {STRING_32} "3" and
+ tb.item ("d") ~ {STRING_32} "test"
+ )
+ )
+
+ if attached cfg.sub_config ("second") as cfg_second then
+ assert ("has_item (is)", cfg_second.has_item ("is"))
+ assert ("item (is)", attached cfg_second.text_item ("is") as v and then v.same_string_general ("2"))
+
+ else
+ assert ("has second", False)
+ end
+ end
+
+
+end
diff --git a/library/email/email-safe.ecf b/library/email/email-safe.ecf
new file mode 100644
index 0000000..55db26a
--- /dev/null
+++ b/library/email/email-safe.ecf
@@ -0,0 +1,17 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
diff --git a/library/email/email_service.e b/library/email/email_service.e
new file mode 100644
index 0000000..1e1c19b
--- /dev/null
+++ b/library/email/email_service.e
@@ -0,0 +1,93 @@
+note
+ description: "Basic Email Service"
+ date: "$Date: 2015-04-30 05:45:25 -0300 (ju. 30 de abr. de 2015) $"
+ revision: "$Revision: 97218 $"
+
+class
+ EMAIL_SERVICE
+
+inherit
+ SHARED_ERROR
+
+ SHARED_LOGGER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_params: like parameters)
+ -- Create instance of {EMAIL_SERVICE} with smtp_server `a_params.smtp_server'.
+ -- Using `a_params.admin_email' as admin email.
+ do
+ parameters := a_params
+ initialize
+ end
+
+ initialize
+ -- Initialize service.
+ do
+ admin_email := parameters.admin_email
+ create {NOTIFICATION_SMTP_MAILER} mailer.make (parameters.smtp_server)
+ set_successful
+ end
+
+ parameters: EMAIL_SERVICE_PARAMETERS
+ -- Associated parameters.
+
+ admin_email: IMMUTABLE_STRING_8
+ -- Site admin's email.
+
+ mailer: NOTIFICATION_MAILER
+ -- SMTP protocol.
+
+feature -- Basic Operations
+
+ send_internal_email (a_content: READABLE_STRING_GENERAL)
+ do
+ send_message (admin_email, admin_email, "Notification Contact", a_content)
+ end
+
+ send_email_internal_server_error (a_content: READABLE_STRING_GENERAL)
+ do
+ send_message (admin_email, admin_email, "Internal Server Error", a_content)
+ end
+
+ send_message (a_from_address, a_to_address: READABLE_STRING_8; a_subjet: READABLE_STRING_GENERAL; a_content: READABLE_STRING_GENERAL)
+ local
+ l_email: NOTIFICATION_EMAIL
+ utf: UTF_CONVERTER
+ do
+ write_debug_log (generator + ".send_message: [from:" + a_from_address + ", to:" + a_to_address + ", subject:" + a_subjet + ", content:" + a_content)
+ create l_email.make (a_from_address, a_to_address, utf.escaped_utf_32_string_to_utf_8_string_8 (a_subjet) , utf.escaped_utf_32_string_to_utf_8_string_8 (a_content))
+ l_email.add_header_line ("MIME-Version:1.0")
+ l_email.add_header_line ("Content-Type: text/html; charset=utf-8")
+ send_email (l_email)
+ end
+
+feature {NONE} -- Implementation
+
+ send_email (a_email: NOTIFICATION_EMAIL)
+ -- Send the email represented by `a_email'.
+ local
+ l_retried: BOOLEAN
+ do
+ if not l_retried then
+ write_information_log (generator + ".send_email Process send email.")
+ mailer.process_email (a_email)
+ write_information_log (generator + ".send_email Email sent.")
+ if mailer.has_error then
+ set_last_error ("smtp_protocol reported an error", generator + ".send_email")
+ else
+ set_successful
+ end
+ else
+ write_error_log (generator + ".send_email Email not send " + last_error_message)
+ end
+ rescue
+ set_last_error_from_exception (generator + ".send_email")
+ l_retried := True
+ retry
+ end
+
+end
diff --git a/library/email/email_service_parameters.e b/library/email/email_service_parameters.e
new file mode 100644
index 0000000..c7d18e1
--- /dev/null
+++ b/library/email/email_service_parameters.e
@@ -0,0 +1,20 @@
+note
+ description: "Basic Email Service customized for cms site"
+ author: ""
+ date: "$Date: 2015-01-16 07:17:14 -0300 (vi. 16 de ene. de 2015) $"
+ revision: "$Revision: 96467 $"
+
+deferred class
+ EMAIL_SERVICE_PARAMETERS
+
+feature -- Access
+
+ smtp_server: IMMUTABLE_STRING_8
+ deferred
+ end
+
+ admin_email: IMMUTABLE_STRING_8
+ deferred
+ end
+
+end
diff --git a/library/gcse/Readme.md b/library/gcse/Readme.md
new file mode 100644
index 0000000..dc80b1f
--- /dev/null
+++ b/library/gcse/Readme.md
@@ -0,0 +1,4 @@
+Google Custom Search Engine Eiffel Lbrary
+
+Based on https://developers.google.com/custom-search/json-api/v1/using_rest
+
diff --git a/library/gcse/gcse-safe.ecf b/library/gcse/gcse-safe.ecf
new file mode 100644
index 0000000..2dec9df
--- /dev/null
+++ b/library/gcse/gcse-safe.ecf
@@ -0,0 +1,21 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/gcse/gcse.ecf b/library/gcse/gcse.ecf
new file mode 100644
index 0000000..463cf6d
--- /dev/null
+++ b/library/gcse/gcse.ecf
@@ -0,0 +1,21 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/gcse/license.lic b/library/gcse/license.lic
new file mode 100644
index 0000000..93c113a
--- /dev/null
+++ b/library/gcse/license.lic
@@ -0,0 +1,10 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR} Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
diff --git a/library/gcse/src/gcse_api.e b/library/gcse/src/gcse_api.e
new file mode 100644
index 0000000..213b3c5
--- /dev/null
+++ b/library/gcse/src/gcse_api.e
@@ -0,0 +1,236 @@
+note
+ description: "[
+ Simple API to call Google Custome Search Engine
+ Example call:
+ GET https://www.googleapis.com/customsearch/v1?key=INSERT_YOUR_API_KEY&cx=017576662512468239146:omuauf_lfve&q=lectures
+ ]"
+ date: "$Date: 2015-10-09 08:11:07 -0300 (vi., 09 oct. 2015) $"
+ revision: "$Revision: 97973 $"
+ EIS: "name=Google Custom Search Engine", "src=https://developers.google.com/custom-search/json-api/v1/using_rest", "protocol=uri"
+
+class
+ GCSE_API
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_query_parameters: GCSE_QUERY_PARAMETERS)
+ -- Create an object GCSE with query_parameters `a_query_parameters'
+ do
+ query_parameter := a_query_parameters
+ ensure
+ query_parameters_set: query_parameter = a_query_parameters
+ end
+
+feature -- Access
+
+ base_uri: STRING_8 = "https://www.googleapis.com/customsearch/v1"
+ -- Google custom search base URI.
+
+ query_parameter: GCSE_QUERY_PARAMETERS
+ -- Google custom search parameters.
+
+ last_result: detachable GCSE_RESPONSE
+ -- Search results.
+
+feature -- Status Reports
+
+ errors: detachable LIST [READABLE_STRING_8]
+ -- optional list of error messages.
+
+feature -- API
+
+ search
+ -- Search
+ local
+ l_parser: JSON_PARSER
+ l_gcse_response: detachable GCSE_RESPONSE
+ do
+ -- Data format for the response.
+ -- At the moment we are using the default value: json
+ -- but it's possible to define atom response using the alt parameter.
+ last_result := Void
+ if attached get as l_response then
+ create l_gcse_response
+ l_gcse_response.set_status (l_response.status)
+ l_gcse_response.set_status_nessage (l_response.status_message)
+
+ if attached l_response.body as l_body then
+ create l_parser.make_with_string (l_body)
+ l_parser.parse_content
+ if l_response.status = 200 and then l_parser.is_parsed and then attached {JSON_OBJECT} l_parser.parsed_json_object as jv then
+ -- Queries
+ if attached {JSON_OBJECT} jv.item (queries_key) as jqueries then
+ -- Next Page
+ if attached {GCSE_PAGE} query_page (next_page_key, jqueries) as l_page then
+ l_gcse_response.set_next_page (l_page)
+ end
+ -- Current Page
+ if attached {GCSE_PAGE} query_page (request_key, jqueries) as l_page then
+ l_gcse_response.set_current_page (l_page)
+ end
+ -- Previous Page
+ if attached {GCSE_PAGE} query_page (previous_page_key, jqueries) as l_page then
+ l_gcse_response.set_previous_page (l_page)
+ end
+ end
+ if attached {JSON_ARRAY} jv.item (items_key) as jitems then
+ across jitems as ic loop
+ if attached{JSON_OBJECT} ic.item as j_item then
+ l_gcse_response.add_item (item (j_item))
+ end
+ end
+ end
+ else
+ put_error (l_body)
+ end
+ else
+ put_error (l_response.status.out)
+ end
+ else
+ put_error ("unknown")
+ end
+ last_result := l_gcse_response
+ end
+
+feature {NONE} -- REST API
+
+ get: detachable RESPONSE
+ -- Reading Data.
+ local
+ l_request: REQUEST
+ do
+ create l_request.make ("GET", new_uri)
+ Result := l_request.execute
+ end
+
+feature {NONE} -- Implementation
+
+ new_uri: STRING_8
+ -- new uri (BaseUri?key=secret_value&cx=a_cx_id&q=a_query
+ -- ?key=INSERT_YOUR_API_KEY&cx=017576662512468239146:omuauf_lfve&q=lectures
+ -- full template BaseUri?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&
+ -- safe={safe?}&cx={cx?}&cref={cref?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&
+ -- googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&
+ -- siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&
+ -- orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&
+ -- searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&
+ -- imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
+
+ do
+ create Result.make_from_string (base_uri)
+ Result.append ("?key=")
+ Result.append (query_parameter.secret)
+ Result.append ("&cx=")
+ Result.append (query_parameter.cx)
+ Result.append ("&q=")
+ Result.append (query_parameter.query)
+ -- num
+ if attached query_parameter.num as l_num then
+ Result.append ("&num=")
+ Result.append (l_num)
+ end
+ if attached query_parameter.start as l_start then
+ Result.append ("&start=")
+ Result.append (l_start)
+ end
+ end
+
+ put_error (a_message: READABLE_STRING_GENERAL)
+ -- put error message `a_message'.
+ local
+ l_errors: like errors
+ utf: UTF_CONVERTER
+ do
+ l_errors := errors
+ if l_errors = Void then
+ create {ARRAYED_LIST [STRING]} l_errors.make (1)
+ errors := l_errors
+ end
+ l_errors.force (utf.utf_32_string_to_utf_8_string_8 (a_message))
+ end
+
+ item (a_item: JSON_OBJECT): GCSE_PAGE_ITEM
+ -- Google Result Metadata Item.
+ do
+ create Result
+ if attached {JSON_STRING} a_item.item ("kind") as l_kind then
+ Result.set_kind (l_kind.item)
+ end
+ if attached {JSON_STRING} a_item.item ("title") as l_title then
+ Result.set_title (l_title.item)
+ end
+ if attached {JSON_STRING} a_item.item ("htmlTitle") as l_htmltitle then
+ Result.set_html_title (l_htmltitle.unescaped_string_32)
+ end
+ if attached {JSON_STRING} a_item.item ("link") as l_link then
+ Result.set_link (l_link.item)
+ end
+ if attached {JSON_STRING} a_item.item ("displayLink") as l_display_link then
+ Result.set_display_link (l_display_link.item)
+ end
+ if attached {JSON_STRING} a_item.item ("snippet") as l_snippet then
+ Result.set_snippet (l_snippet.unescaped_string_8)
+ end
+ if attached {JSON_STRING} a_item.item ("htmlSnippet") as l_html_snippet then
+ Result.set_html_snippet (l_html_snippet.unescaped_string_32)
+ end
+ if attached {JSON_STRING} a_item.item ("formattedUrl") as l_formatted_url then
+ Result.set_formatted_url (l_formatted_url.item)
+ end
+ end
+
+ query_page (a_page_key: JSON_STRING; a_queries: JSON_OBJECT): detachable GCSE_PAGE
+ -- Google result medata query. Return a query page based for a query with page key `a_page_key', if any.
+ do
+ if
+ attached {JSON_ARRAY} a_queries.item (a_page_key) as jquerypage and then
+ jquerypage.count > 0 and then
+ attached {JSON_OBJECT} jquerypage.i_th (1) as jpage
+ then
+ create Result
+ if attached {JSON_STRING} jpage.item ("title") as l_title then
+ Result.set_title (l_title.item)
+ end
+ if attached {JSON_STRING} jpage.item ("totalResults") as l_results then
+ Result.set_total_results (l_results.item.to_integer)
+ end
+ if attached {JSON_STRING} jpage.item ("searchTerms") as l_search_terms then
+ Result.set_search_terms (l_search_terms.item)
+ end
+ -- TODO check if we should use INTEGER_64
+ if attached {JSON_NUMBER} jpage.item ("count") as l_count then
+ Result.set_count (l_count.integer_64_item.as_integer_32)
+ end
+ if attached {JSON_NUMBER} jpage.item ("startIndex") as l_index then
+ Result.set_start_index (l_index.integer_64_item.as_integer_32)
+ end
+ end
+ end
+
+feature {NONE} -- JSON Keys
+
+ queries_key: STRING = "queries"
+
+ next_page_key: STRING = "nextPage"
+
+ request_key: STRING = "request"
+
+ previous_page_key: STRING = "previousPage"
+
+ items_key: STRING = "items"
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/gcse/src/gcse_page.e b/library/gcse/src/gcse_page.e
new file mode 100644
index 0000000..5387e75
--- /dev/null
+++ b/library/gcse/src/gcse_page.e
@@ -0,0 +1,118 @@
+note
+ description: "Represent metadata describing the query for the current set of results."
+ date: "$Date: 2015-10-09 08:11:07 -0300 (vi., 09 oct. 2015) $"
+ revision: "$Revision: 97973 $"
+
+class
+ GCSE_PAGE
+
+inherit
+
+ DEBUG_OUTPUT
+
+feature -- Access
+
+ search_terms: detachable STRING_8
+ -- search term
+
+ title: detachable STRING_8
+ -- Search title.
+
+ total_results: INTEGER
+ -- Search total results.
+
+ count: INTEGER
+ -- Rows per page.
+
+ start_index: INTEGER
+ -- Page index.
+
+feature -- Element change
+
+ set_search_terms (a_search_terms: like search_terms)
+ -- Assign `search_terms' with `a_search_terms'.
+ do
+ search_terms := a_search_terms
+ ensure
+ search_terms_assigned: search_terms = a_search_terms
+ end
+
+feature -- Change element
+
+ set_title (a_title: like title)
+ -- Set title with `a_title'
+ do
+ title := a_title
+ ensure
+ title_set: title = a_title
+ end
+
+ set_total_results (a_total_results: like total_results)
+ -- Set total_results with `a_total_results'.
+ do
+ total_results := a_total_results
+ ensure
+ total_results_set: total_results = a_total_results
+ end
+
+ set_count (a_count: like count)
+ -- Set count with `a_count'.
+ do
+ count := a_count
+ ensure
+ count_set: count = a_count
+ end
+
+ set_start_index (a_start_index: like start_index)
+ -- Set start_index with `a_start_index'.
+ do
+ start_index := a_start_index
+ ensure
+ start_index_set: start_index = a_start_index
+ end
+
+feature -- Status report
+
+ debug_output: STRING_8
+ --
+ do
+ create Result.make_from_string ("%NPage details%N")
+ if attached title as l_title then
+ Result.append ("Title:")
+ Result.append (l_title)
+ Result.append_character ('%N')
+ end
+ if attached search_terms as l_search_tearm then
+ Result.append ("Search Tearm:")
+ Result.append (l_search_tearm)
+ Result.append_character ('%N')
+ end
+
+ Result.append ("Count:")
+ Result.append (count.out)
+ Result.append_character ('%N')
+ Result.append ("Total Result:")
+ Result.append (count.out)
+ Result.append_character ('%N')
+ Result.append ("Count:")
+ Result.append (total_results.out)
+ Result.append_character ('%N')
+ Result.append ("Start index:")
+ Result.append (start_index.out)
+ Result.append_character ('%N')
+
+ end
+
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/gcse/src/gcse_page_item.e b/library/gcse/src/gcse_page_item.e
new file mode 100644
index 0000000..97d6b1d
--- /dev/null
+++ b/library/gcse/src/gcse_page_item.e
@@ -0,0 +1,208 @@
+note
+ description: "Represent a search result, include the URL, title and text snippets that describe the result"
+ date: "$Date: 2015-10-09 08:11:07 -0300 (vi., 09 oct. 2015) $"
+ revision: "$Revision: 97973 $"
+
+class
+ GCSE_PAGE_ITEM
+
+inherit
+ DEBUG_OUTPUT
+
+feature -- Access
+
+ html_formatted_url: detachable STRING_8
+ -- Html formatted url of this result
+
+ formatted_url: detachable STRING_8
+ -- Formatted url of this result
+
+ cache_id: detachable STRING_8
+ -- Cache id of this result
+
+ html_snippet: detachable STRING_8
+ -- Html snippet of this request
+
+ snippet: detachable STRING_8
+ -- Snippet of this result
+
+ display_link: detachable STRING_8
+ -- Display link of this result
+
+ link: detachable STRING_8
+ -- link of this result
+
+ html_title: detachable STRING_8
+ -- html title of result
+
+ title: detachable STRING_8
+ -- title of result.
+
+ kind: detachable STRING_8
+ -- Kind of actual search result.
+
+ page_map: detachable GCSE_PAGE_MAP
+ -- Page map
+ --! Not supported for now.
+
+feature -- Element change
+
+ set_html_formatted_url (a_html_formatted_url: like html_formatted_url)
+ -- Assign `html_formatted_url' with `a_html_formatted_url'.
+ do
+ html_formatted_url := a_html_formatted_url
+ ensure
+ html_formatted_url_assigned: html_formatted_url = a_html_formatted_url
+ end
+
+ set_formatted_url (a_formatted_url: like formatted_url)
+ -- Assign `formatted_url' with `a_formatted_url'.
+ do
+ formatted_url := a_formatted_url
+ ensure
+ formatted_url_assigned: formatted_url = a_formatted_url
+ end
+
+ set_cache_id (a_cache_id: like cache_id)
+ -- Assign `cache_id' with `a_cache_id'.
+ do
+ cache_id := a_cache_id
+ ensure
+ cache_id_assigned: cache_id = a_cache_id
+ end
+
+ set_html_snippet (a_html_snippet: like html_snippet)
+ -- Assign `html_snippet' with `a_html_snippet'.
+ do
+ html_snippet := a_html_snippet
+ ensure
+ html_snippet_assigned: html_snippet = a_html_snippet
+ end
+
+ set_snippet (a_snippet: like snippet)
+ -- Assign `snippet' with `a_snippet'.
+ do
+ snippet := a_snippet
+ ensure
+ snippet_assigned: snippet = a_snippet
+ end
+
+ set_display_link (a_display_link: like display_link)
+ -- Assign `display_link' with `a_display_link'.
+ do
+ display_link := a_display_link
+ ensure
+ display_link_assigned: display_link = a_display_link
+ end
+
+ set_link (a_link: like link)
+ -- Assign `link' with `a_link'.
+ do
+ link := a_link
+ ensure
+ link_assigned: link = a_link
+ end
+
+ set_html_title (a_html_title: like html_title)
+ -- Assign `html_title' with `a_html_title'.
+ do
+ html_title := a_html_title
+ ensure
+ html_title_assigned: html_title = a_html_title
+ end
+
+ set_title (a_title: like title)
+ -- Assign `title' with `a_title'.
+ do
+ title := a_title
+ ensure
+ title_assigned: title = a_title
+ end
+
+ set_kind (a_kind: like kind)
+ -- Assign `kind' with `a_kind'.
+ do
+ kind := a_kind
+ ensure
+ kind_assigned: kind = a_kind
+ end
+
+ set_page_map (a_map: like page_map)
+ -- Assign `kind' with `a_kind'.
+ do
+ page_map := a_map
+ ensure
+ page_map_assigned: page_map = a_map
+ end
+
+
+feature -- Output
+
+ debug_output: STRING_8
+ --
+ do
+ create Result.make_from_string ("%NPage Item details%N")
+ if attached title as l_title then
+ Result.append ("Title:")
+ Result.append (l_title)
+ Result.append_character ('%N')
+ end
+ if attached kind as l_kind then
+ Result.append ("Kind:")
+ Result.append (l_kind)
+ Result.append_character ('%N')
+ end
+ if attached html_title as l_html_title then
+ Result.append ("Html title:")
+ Result.append (l_html_title)
+ Result.append_character ('%N')
+ end
+ if attached link as l_link then
+ Result.append ("Link:")
+ Result.append (l_link)
+ Result.append_character ('%N')
+ end
+ if attached display_link as l_display_link then
+ Result.append ("Display link:")
+ Result.append (l_display_link)
+ Result.append_character ('%N')
+ end
+ if attached snippet as l_snippet then
+ Result.append ("Snippet:")
+ Result.append (l_snippet)
+ Result.append_character ('%N')
+ end
+ if attached html_snippet as l_html_snippet then
+ Result.append ("Html snippet:")
+ Result.append (l_html_snippet)
+ Result.append_character ('%N')
+ end
+ if attached cache_id as l_cache_id then
+ Result.append ("Cache_id:")
+ Result.append (l_cache_id)
+ Result.append_character ('%N')
+ end
+ if attached formatted_url as l_formatted_url then
+ Result.append ("Formatted url:")
+ Result.append (l_formatted_url)
+ Result.append_character ('%N')
+ end
+ if attached html_formatted_url as l_html_formatted_url then
+ Result.append ("Html formatted url:")
+ Result.append (l_html_formatted_url)
+ Result.append_character ('%N')
+ end
+
+ end
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/gcse/src/gcse_page_map.e b/library/gcse/src/gcse_page_map.e
new file mode 100644
index 0000000..9b5f099
--- /dev/null
+++ b/library/gcse/src/gcse_page_map.e
@@ -0,0 +1,41 @@
+note
+ description: "[
+ Represent a google page map
+ "pagemap": {
+ "cse_image": [
+ {
+ "src": "https://www.eiffel.org/portal/files/userpictures/picture-40.jpg"
+ }
+ ],
+ "cse_thumbnail": [
+ {
+ "width": "81",
+ "height": "61",
+ "src": "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcRnC-RKzps6BFItx_MLYBVskFI7U6u0y3VJBInomPYEF5sO6gkip94mLw"
+ }
+ ]
+ }
+ ]"
+ date: "$Date: 2015-10-09 08:11:07 -0300 (vi., 09 oct. 2015) $"
+ revision: "$Revision: 97973 $"
+ EIS: "name=PageMaps", "src=https://developers.google.com/custom-search/docs/structured_data#pagemaps", "protocol=url"
+
+class
+ GCSE_PAGE_MAP
+
+feature -- Access
+
+
+
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/gcse/src/gcse_query_parameters.e b/library/gcse/src/gcse_query_parameters.e
new file mode 100644
index 0000000..6d8c18e
--- /dev/null
+++ b/library/gcse/src/gcse_query_parameters.e
@@ -0,0 +1,238 @@
+note
+ description: "[
+ Represent google custom search parameters
+ Example url template
+ "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&cref={cref?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
+ ]"
+ optional_parameters: "[
+ Optional parameters
+ c2coff string Enables or disables Simplified and Traditional Chinese Search.
+ The default value for this parameter is 0 (zero), meaning that the feature is enabled. Supported values are:
+ 1: Disabled
+ 0: Enabled (default)
+ cr string Restricts search results to documents originating in a particular country.
+ You may use Boolean operators in the cr parameter's value.
+ Google Search determines the country of a document by analyzing:
+ the top-level domain (TLD) of the document's URL
+ the geographic location of the Web server's IP address
+ See the Country Parameter Values page for a list of valid values for this parameter.
+ cref string The URL of a linked custom search engine specification to use for this request.
+ Does not apply for Google Site Search
+ If both cx and cref are specified, the cx value is used
+ cx string The custom search engine ID to use for this request.
+ If both cx and cref are specified, the cx value is used.
+ dateRestrict string Restricts results to URLs based on date. Supported values include:
+ d[number]: requests results from the specified number of past days.
+ w[number]: requests results from the specified number of past weeks.
+ m[number]: requests results from the specified number of past months.
+ y[number]: requests results from the specified number of past years.
+ exactTerms string Identifies a phrase that all documents in the search results must contain.
+ excludeTerms string Identifies a word or phrase that should not appear in any documents in the search results.
+ fileType string Restricts results to files of a specified extension. A list of file types indexable by Google can be found in Webmaster Tools Help Center.
+ filter string Controls turning on or off the duplicate content filter.
+ See Automatic Filtering for more information about Google's search results filters. Note that host crowding filtering applies only to multi-site searches.
+ By default, Google applies filtering to all search results to improve the quality of those results.
+
+
+ Acceptable values are:
+ "0": Turns off duplicate content filter.
+ "1": Turns on duplicate content filter.
+ gl string Geolocation of end user.
+ The gl parameter value is a two-letter country code. The gl parameter boosts search results whose country of origin matches the parameter value. See the Country Codes page for a list of valid values.
+ Specifying a gl parameter value should lead to more relevant results. This is particularly true for international customers and, even more specifically, for customers in English- speaking countries other than the United States.
+ googlehost string The local Google domain (for example, google.com, google.de, or google.fr) to use to perform the search.
+ highRange string
+ Specifies the ending value for a search range.
+ Use lowRange and highRange to append an inclusive search range of lowRange...highRange to the query.
+ hl string Sets the user interface language.
+ Explicitly setting this parameter improves the performance and the quality of your search results.
+ See the Interface Languages section of Internationalizing Queries and Results Presentation for more information, and Supported Interface Languages for a list of supported languages.
+ hq string Appends the specified query terms to the query, as if they were combined with a logical AND operator.
+ imgColorType string Returns black and white, grayscale, or color images: mono, gray, and color.
+
+ Acceptable values are:
+ "color": color
+ "gray": gray
+ "mono": mono
+ imgDominantColor string Returns images of a specific dominant color.
+
+ Acceptable values are:
+ "black": black
+ "blue": blue
+ "brown": brown
+ "gray": gray
+ "green": green
+ "pink": pink
+ "purple": purple
+ "teal": teal
+ "white": white
+ "yellow": yellow
+ imgSize string Returns images of a specified size.
+
+ Acceptable values are:
+ "huge": huge
+ "icon": icon
+ "large": large
+ "medium": medium
+ "small": small
+ "xlarge": xlarge
+ "xxlarge": xxlarge
+ imgType string Returns images of a type.
+
+ Acceptable values are:
+ "clipart": clipart
+ "face": face
+ "lineart": lineart
+ "news": news
+ "photo": photo
+ linkSite string Specifies that all search results should contain a link to a particular URL
+ lowRange string Specifies the starting value for a search range.
+ Use lowRange and highRange to append an inclusive search range of lowRange...highRange to the query.
+ lr string Restricts the search to documents written in a particular language (e.g., lr=lang_ja).
+
+ Acceptable values are:
+ "lang_ar": Arabic
+ "lang_bg": Bulgarian
+ "lang_ca": Catalan
+ "lang_cs": Czech
+ "lang_da": Danish
+ "lang_de": German
+ "lang_el": Greek
+ "lang_en": English
+ "lang_es": Spanish
+ "lang_et": Estonian
+ "lang_fi": Finnish
+ "lang_fr": French
+ "lang_hr": Croatian
+ "lang_hu": Hungarian
+ "lang_id": Indonesian
+ "lang_is": Icelandic
+ "lang_it": Italian
+ "lang_iw": Hebrew
+ "lang_ja": Japanese
+ "lang_ko": Korean
+ "lang_lt": Lithuanian
+ "lang_lv": Latvian
+ "lang_nl": Dutch
+ "lang_no": Norwegian
+ "lang_pl": Polish
+ "lang_pt": Portuguese
+ "lang_ro": Romanian
+ "lang_ru": Russian
+ "lang_sk": Slovak
+ "lang_sl": Slovenian
+ "lang_sr": Serbian
+ "lang_sv": Swedish
+ "lang_tr": Turkish
+ "lang_zh-CN": Chinese (Simplified)
+ "lang_zh-TW": Chinese (Traditional)
+
+ orTerms string Provides additional search terms to check for in a document, where each document in the search results must contain at least one of the additional search terms.
+ relatedSite string Specifies that all search results should be pages that are related to the specified URL.
+ rights string Filters based on licensing. Supported values include: cc_publicdomain, cc_attribute, cc_sharealike, cc_noncommercial, cc_nonderived, and combinations of these.
+ safe string Search safety level.
+
+ Acceptable values are:
+ "high": Enables highest level of SafeSearch filtering.
+ "medium": Enables moderate SafeSearch filtering.
+ "off": Disables SafeSearch filtering. (default)
+ searchType string Specifies the search type: image. If unspecified, results are limited to webpages.
+
+ Acceptable values are:
+ "image": custom image search.
+ siteSearch string Specifies all search results should be pages from a given site.
+ siteSearchFilter string Controls whether to include or exclude results from the site named in the siteSearch parameter.
+
+ Acceptable values are:
+ "e": exclude
+ "i": include
+ sort string The sort expression to apply to the results.
+]"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+ EIS: "GCSE parameters", "src=https://developers.google.com/custom-search/json-api/v1/reference/cse/list", "protocol=URI"
+
+class
+ GCSE_QUERY_PARAMETERS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_secret_key, a_cx, a_query: READABLE_STRING_8)
+ -- Create an object GCSE_QUERY_PARAMETERS with secret key `a_secret_key' and a custom search engine id `a_cx'.
+ -- and query `a_query'.
+ do
+ -- TODO
+ -- At the moment the API only use cx as Google Custom Search id.
+ -- Custom search engine ID - Use either cx or cref to specify the custom search engine you want to use to perform this search
+
+ secret := a_secret_key
+ cx := a_cx
+ query := a_query
+ ensure
+ secret_set: secret.same_string (a_secret_key)
+ cx_set: cx.same_string (a_cx)
+ query_set: query.same_string (a_query)
+ end
+
+feature -- Access : Required Parameters
+
+ secret: READABLE_STRING_8
+ -- Required. The shared key between your site and Google Custom Search Engine.
+
+ cx: READABLE_STRING_8
+ -- Custom search engine id to perform this search.
+
+ query: READABLE_STRING_8
+ -- Search query, query parameter to specify your search expression.
+
+feature -- Optional Parameters
+
+
+
+ num : detachable STRING_8
+ -- Number of search results to return.
+ -- Valid values are integers between 1 and 10, inclusive.
+
+ start: detachable STRING_8
+ -- The index of the first result to return.
+
+
+feature -- Change Elements
+
+ set_num (a_num: READABLE_STRING_8)
+ require
+ is_number: a_num.is_integer
+ valid_range: a_num.to_integer >= 1 and then a_num.to_integer <= 10
+ do
+ num := a_num
+ ensure
+ num_set: num = a_num
+ valid_rage_set: attached num as l_num and then l_num.to_integer >= 1 and then l_num.to_integer <= 10
+ end
+
+
+ set_start (a_start: READABLE_STRING_8)
+ require
+ is_number: a_start.is_integer
+ valid_start: a_start.to_integer >= 1
+ do
+ start := a_start
+ ensure
+ start_set: start = a_start
+ valid_start_set: attached start as l_start and then l_start.to_integer >= 1
+ end
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/gcse/src/gcse_response.e b/library/gcse/src/gcse_response.e
new file mode 100644
index 0000000..32fdb2e
--- /dev/null
+++ b/library/gcse/src/gcse_response.e
@@ -0,0 +1,115 @@
+note
+ description: "[
+ Represent search request metadata
+ URL: search template used for the current results.
+ Queries: current, next and previous page.
+ Context
+ Search infromation
+ Items: array of actual search results.
+ ]"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+
+class
+ GCSE_RESPONSE
+
+ --! TODO
+ --! All suppport for for url, context and search information.
+
+feature -- Access
+
+ current_page: detachable GCSE_PAGE
+ -- Metadata describing the query for the current set of results.
+ -- This role is always present in the response.
+ -- It is always an array with just one element.
+
+ next_page: detachable GCSE_PAGE
+ -- Metadata describing the query to use for the next page of results.
+ -- This role is not present if the current results are the last page. Note: This API returns up to the first 100 results only.
+ -- When present, it is always a array with just one element.
+
+ previous_page: detachable GCSE_PAGE
+ -- Metadata describing the query to use for the previous page of results.
+ -- Not present if the current results are the first page.
+ -- When present, it is always a array with just one element.
+
+ items: detachable LIST [GCSE_PAGE_ITEM]
+ -- Contains the actual search results. The search results include the URL, title and text snippets that describe the result.
+
+feature -- Change Element
+
+ set_current_page (a_page: GCSE_PAGE)
+ -- Set `current_page' with `a_page'.
+ do
+ current_page := a_page
+ ensure
+ current_page_set: current_page = a_page
+ end
+
+ set_next_page (a_page: GCSE_PAGE)
+ -- Set `next_page' with `a_page'.
+ do
+ next_page := a_page
+ ensure
+ next_page_set: next_page = a_page
+ end
+
+ set_previous_page (a_page: GCSE_PAGE)
+ -- Set `previous_page' with `a_page'.
+ do
+ previous_page := a_page
+ ensure
+ previous_page_set: previous_page = a_page
+ end
+
+ add_item (a_item: GCSE_PAGE_ITEM)
+ -- Add item `a_item' to the list of items.
+ local
+ l_items: like items
+ do
+ l_items := items
+ if l_items = Void then
+ create {ARRAYED_LIST[GCSE_PAGE_ITEM]}l_items.make (10)
+ items := l_items
+ end
+ l_items.force (a_item)
+ end
+
+
+feature -- Acess: HTTP Response
+
+ status: INTEGER
+ -- HTTP status code.
+
+ status_message: detachable READABLE_STRING_8
+ -- associated textual phrase for the response status.
+
+feature -- Change Element: HTTP Response
+
+ set_status (a_status: like status)
+ -- Set `status' with `a_status'.
+ do
+ status := a_status
+ ensure
+ status_set: status = a_status
+ end
+
+ set_status_nessage (a_message: like status_message)
+ -- Set `status_message' with `a_message'.
+ do
+ status_message := a_message
+ ensure
+ status_message_set: status_message = a_message
+ end
+
+;note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/library/gcse/test/application.e b/library/gcse/test/application.e
new file mode 100644
index 0000000..14c7d46
--- /dev/null
+++ b/library/gcse/test/application.e
@@ -0,0 +1,80 @@
+note
+ description : "test application root class"
+ date : "$Date: 2015-12-02 10:27:38 -0300 (mi. 02 de dic. de 2015) $"
+ revision : "$Revision: 98180 $"
+
+class
+ APPLICATION
+
+inherit
+ ARGUMENTS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Run application.
+ local
+ gcse: GCSE_API
+ l_parameters: GCSE_QUERY_PARAMETERS
+ do
+ create l_parameters.make (key, cx, "scoop")
+ create gcse.make (l_parameters)
+ gcse.search
+
+ if attached {GCSE_RESPONSE} gcse.last_result as l_result then
+ if attached l_result.current_page as l_page then
+ print ("Current Page%N")
+ print (l_page.debug_output)
+ end
+ if attached l_result.next_page as l_page then
+ print ("Next Page%N")
+ print (l_page.debug_output)
+ end
+ if attached l_result.previous_page as l_page then
+ print ("Previous Page%N")
+ print (l_page.debug_output)
+ end
+
+ if attached l_result.items as l_items then
+ print ("Number of items:" + l_items.count.out)
+ across l_items as ic loop print (ic.item.debug_output) end
+ end
+
+ if attached l_result.next_page as l_page then
+ l_parameters.set_start (l_page.start_index.out)
+ gcse.search
+ end
+ end
+
+ if attached {GCSE_RESPONSE} gcse.last_result as l_result then
+ if attached l_result.current_page as l_page then
+ print ("Current Page%N")
+ print (l_page.debug_output)
+ end
+ if attached l_result.next_page as l_page then
+ print ("Next Page%N")
+ print (l_page.debug_output)
+ end
+ if attached l_result.previous_page as l_page then
+ print ("Previous Page%N")
+ print (l_page.debug_output)
+ end
+
+ if attached l_result.items as l_items then
+ print ("Number of items:" + l_items.count.out)
+ across l_items as ic loop print (ic.item.debug_output) end
+ end
+
+ end
+
+
+ end
+
+feature {NONE} -- Implementation
+
+ Key: STRING = "AIzaSyBKAXNofo-RqZb6kUmpbiCwPEy7n7-E51k"
+ cx : STRING = "015017565055626880074:9gdgp1fvt-g"
+end
diff --git a/library/gcse/test/gcse_api_test_set.e b/library/gcse/test/gcse_api_test_set.e
new file mode 100644
index 0000000..02deb68
--- /dev/null
+++ b/library/gcse/test/gcse_api_test_set.e
@@ -0,0 +1,31 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+ testing: "type/manual"
+
+class
+ GCSE_API_TEST_SET
+
+inherit
+ EQA_TEST_SET
+
+feature -- Test routines
+
+
+feature {NONE} -- Implementation
+
+ has_error (l_captcha: GCSE_API; a_error: READABLE_STRING_32): BOOLEAN
+ do
+ if attached l_captcha.errors as l_errors then
+ l_errors.compare_objects
+ Result := l_errors.has (a_error)
+ end
+ end
+
+end
+
+
diff --git a/library/gcse/test/test.ecf b/library/gcse/test/test.ecf
new file mode 100644
index 0000000..eb24953
--- /dev/null
+++ b/library/gcse/test/test.ecf
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/http_client_extension/http_client_extension-safe.ecf b/library/http_client_extension/http_client_extension-safe.ecf
new file mode 100644
index 0000000..83979b3
--- /dev/null
+++ b/library/http_client_extension/http_client_extension-safe.ecf
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/http_client_extension/http_client_extension.ecf b/library/http_client_extension/http_client_extension.ecf
new file mode 100644
index 0000000..f15887d
--- /dev/null
+++ b/library/http_client_extension/http_client_extension.ecf
@@ -0,0 +1,25 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/http_client_extension/src/request/request.e b/library/http_client_extension/src/request/request.e
new file mode 100644
index 0000000..5d4713f
--- /dev/null
+++ b/library/http_client_extension/src/request/request.e
@@ -0,0 +1,199 @@
+note
+ description: "Represent an HTTP request."
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+
+class
+ REQUEST
+
+inherit
+
+ HTTP_CONSTANTS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_method: READABLE_STRING_8; a_uri: READABLE_STRING_8)
+ require
+ valid_http_method: is_http_method (a_method)
+ valid_uri: is_valid_uri (a_uri)
+ do
+ verb := a_method
+ uri := a_uri
+ create headers.make (5)
+ ensure
+ ver_set: verb = a_method
+ uri_set: uri = a_uri
+ end
+
+feature -- Status Report
+
+ is_valid_uri (a_uri: READABLE_STRING_8): BOOLEAN
+ local
+ l_uri: URI
+ do
+ create l_uri.make_from_string (a_uri)
+ Result := l_uri.is_valid
+ end
+
+ query_string: detachable READABLE_STRING_8
+ local
+ l_uri: URI
+ do
+ create l_uri.make_from_string (uri)
+ Result := l_uri.query
+ end
+
+ sanitized_url: READABLE_STRING_8
+ -- Returns the URL without the query string part
+ local
+ l_uri: URI
+ do
+ create l_uri.make_from_string (uri)
+ l_uri.remove_query
+ Result := l_uri.string
+ ensure
+ sanitized: not as_uri (Result).has_query
+ end
+
+ is_http_method (a_method: READABLE_STRING_GENERAL): BOOLEAN
+ do
+ if a_method.same_string (method_connect) then
+ Result := True
+ elseif a_method.same_string (method_delete) then
+ Result := True
+ elseif a_method.same_string (method_get) then
+ Result := True
+ elseif a_method.same_string (method_head) then
+ Result := True
+ elseif a_method.same_string (method_options) then
+ Result := True
+ elseif a_method.same_string (method_patch) then
+ Result := True
+ elseif a_method.same_string (method_post) then
+ Result := True
+ elseif a_method.same_string (method_put) then
+ Result := True
+ elseif a_method.same_string (method_trace) then
+ Result := True
+ end
+ end
+
+feature -- Constants
+
+ content_type_header_name: STRING_8 = "Content-Type";
+
+ default_content_type: STRING
+ once
+ Result := application_json
+ end
+
+feature -- Access
+
+ uri: READABLE_STRING_8
+
+ verb: READABLE_STRING_8
+
+ headers: STRING_TABLE [READABLE_STRING_8]
+
+ payload: detachable READABLE_STRING_8
+
+ executor: detachable REQUEST_EXECUTOR
+
+feature -- Change Element
+
+ add_payload (a_payload: like payload)
+ do
+ payload := a_payload
+ ensure
+ payload_set: attached payload as l_payload implies l_payload = a_payload
+ end
+
+ add_header (key: READABLE_STRING_8; value: READABLE_STRING_8)
+ do
+ headers.force (value, key)
+ end
+
+feature -- Execute
+
+ execute: detachable RESPONSE
+ do
+ initialize_executor
+ Result := execute_request
+ end
+
+ initialize_executor
+ do
+ create executor.make (uri, verb)
+ end
+
+feature {NONE} -- Implementation
+
+ execute_request: detachable RESPONSE
+ do
+ if attached executor as l_executor then
+ -- add headers
+ add_headers (l_executor)
+ if verb.same_string (method_put) or else verb.same_string (method_post) or else verb.same_string (method_patch) then
+ l_executor.set_body (body_contents)
+ end
+ if not l_executor.context_executor.headers.has (content_type_header_name) then
+ l_executor.context_executor.add_header (content_type_header_name, default_content_type)
+ end
+ if attached l_executor.execute as l_response then
+ create Result.make (l_response)
+ end
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ add_headers (a_executor: REQUEST_EXECUTOR)
+ local
+ l_context_executor: HTTP_CLIENT_REQUEST_CONTEXT
+ s: READABLE_STRING_GENERAL
+ utf: UTF_CONVERTER
+ do
+ l_context_executor := a_executor.context_executor
+ across
+ headers as ic
+ loop
+ s := ic.key
+ if s.is_valid_as_string_8 then
+ l_context_executor.add_header (s.as_string_8, ic.item)
+ else
+ l_context_executor.add_header (utf.utf_32_string_to_utf_8_string_8 (s), ic.item)
+ end
+ end
+ end
+
+ body_contents: READABLE_STRING_8
+ do
+ if attached payload as l_payload then
+ Result := l_payload
+ else
+ Result := ""
+ end
+ end
+
+ as_uri (a_string: READABLE_STRING_8): URI
+ require
+ is_valid_uri: is_valid_uri (a_string)
+ do
+ create Result.make_from_string (a_string)
+ end
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/http_client_extension/src/request/request_executor.e b/library/http_client_extension/src/request/request_executor.e
new file mode 100644
index 0000000..0e0e5d0
--- /dev/null
+++ b/library/http_client_extension/src/request/request_executor.e
@@ -0,0 +1,97 @@
+note
+ description: "Executes an HTTP request"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+
+class
+ REQUEST_EXECUTOR
+
+inherit
+
+ HTTP_CLIENT_HELPER
+
+ HTTP_CONSTANTS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_url: READABLE_STRING_8; a_method: READABLE_STRING_8)
+ do
+ set_base_url (a_url)
+ verb := a_method
+ ensure
+ base_url_set: base_url.same_string (a_url)
+ method_set: verb.same_string (a_method)
+ end
+
+ set_base_url (a_url: READABLE_STRING_8)
+ -- Set base_url with `a_url'
+ local
+ s: STRING
+ do
+ create s.make_from_string (a_url)
+ s.left_adjust
+ s.right_adjust
+ base_url := s
+ ensure
+ base_url_set: a_url.has_substring (base_url)
+ end
+
+feature -- Access
+
+ verb: READABLE_STRING_8
+ -- HTTP METHOD (Get, Post, ...)
+
+ body: detachable READABLE_STRING_8
+ -- body content
+
+feature -- Element Change
+
+ set_body (a_body: like body)
+ -- Set body with `a_body'.
+ do
+ body := a_body
+ ensure
+ body_set: body = a_body
+ end
+
+feature -- Execute
+
+ execute: detachable HTTP_CLIENT_RESPONSE
+ -- Http executor
+ do
+ if verb.same_string (method_connect) then
+ Result := Void -- not supported for now
+ elseif verb.same_string (method_delete) then
+ Result := execute_delete ("")
+ elseif verb.same_string (method_get) then
+ Result := execute_get ("")
+ elseif verb.same_string (method_head) then
+ Result := Void
+ elseif verb.same_string (method_options) then
+ Result := Void
+ elseif verb.same_string (method_patch) then
+ Result := execute_patch ("", body)
+ elseif verb.same_string (method_post) then
+ Result := execute_post ("", body)
+ elseif verb.same_string (method_put) then
+ Result := execute_put ("", body)
+ elseif verb.same_string (method_trace) then
+ Result := Void
+ end
+ end
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/http_client_extension/src/response/response.e b/library/http_client_extension/src/response/response.e
new file mode 100644
index 0000000..4ee711a
--- /dev/null
+++ b/library/http_client_extension/src/response/response.e
@@ -0,0 +1,57 @@
+note
+ description: "Represent and HTTP Response"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+
+class
+ RESPONSE
+
+create
+ make
+
+feature {NONE} --Initialization
+
+ make (a_response: HTTP_CLIENT_RESPONSE)
+ do
+ http_response := a_response
+ body := a_response.body
+ status := a_response.status
+ headers := a_response.headers
+ status_message := a_response.status_line
+ error_message := a_response.error_message
+ ensure
+ http_reponse_set: http_response = a_response
+ headers_set: headers = a_response.headers
+ status_set: status = a_response.status
+ status_message_set: status_message = a_response.status_line
+ error_message_set: error_message = a_response.error_message
+ end
+
+feature -- Access
+
+ status: INTEGER
+
+ status_message: detachable READABLE_STRING_8
+
+ error_message: detachable READABLE_STRING_8
+
+ body: detachable READABLE_STRING_8
+
+ headers: LIST [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]]
+
+feature {NONE} -- Implementation
+
+ http_response: HTTP_CLIENT_RESPONSE;
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/http_client_extension/src/util/http_client_helper.e b/library/http_client_extension/src/util/http_client_helper.e
new file mode 100644
index 0000000..f120da2
--- /dev/null
+++ b/library/http_client_extension/src/util/http_client_helper.e
@@ -0,0 +1,99 @@
+note
+ description: "Wrapper class for HTTP_CLIENT_SESSION"
+ date: "$Date: 2015-10-08 07:51:29 -0300 (ju., 08 oct. 2015) $"
+ revision: "$Revision: 97966 $"
+
+deferred class
+ HTTP_CLIENT_HELPER
+
+feature -- Access
+
+ http_session: detachable HTTP_CLIENT_SESSION
+
+ get_http_session
+ local
+ h: LIBCURL_HTTP_CLIENT
+ b: like base_url
+ do
+ create h.make
+ b := base_url
+ if b = Void then
+ b := ""
+ end
+ if attached {HTTP_CLIENT_SESSION} h.new_session (base_url) as sess then
+ http_session := sess
+ sess.set_timeout (-1)
+ sess.set_connect_timeout (-1)
+ sess.set_is_insecure (True)
+ sess.set_any_auth_type
+ debug ("curl")
+ sess.set_is_debug (True)
+ end
+ debug ("proxy8888")
+ sess.set_proxy ("127.0.0.1", 8888) --| inspect traffic with http://www.fiddler2.com/
+ end
+ end
+ end
+
+feature -- HTTP client helpers
+
+ execute_get (command_name: READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
+ do
+ get_http_session
+ if attached http_session as sess then
+ Result := sess.get (command_name, context_executor)
+ end
+ end
+
+ execute_post (command_name: READABLE_STRING_8; data: detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
+ do
+ get_http_session
+ if attached http_session as sess then
+ Result := sess.post (command_name, context_executor, data)
+ end
+ end
+
+ execute_delete (command_name: READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
+ do
+ get_http_session
+ if attached http_session as sess then
+ Result := sess.delete (command_name, context_executor)
+ end
+ end
+
+ execute_put (command_name: READABLE_STRING_8; data: detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
+ do
+ get_http_session
+ if attached http_session as sess then
+ Result := sess.put (command_name, context_executor, data)
+ end
+ end
+
+ execute_patch (command_name: READABLE_STRING_8; data: detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
+ do
+ get_http_session
+ if attached http_session as sess then
+ Result := sess.patch (command_name, context_executor, data)
+ end
+ end
+
+ context_executor: HTTP_CLIENT_REQUEST_CONTEXT
+ -- request context for each request
+ once
+ create Result.make
+ end
+
+ base_url: STRING;
+
+note
+ copyright: "2011-2015 Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+
+end
diff --git a/library/layout/layout-safe.ecf b/library/layout/layout-safe.ecf
new file mode 100644
index 0000000..4277dde
--- /dev/null
+++ b/library/layout/layout-safe.ecf
@@ -0,0 +1,3 @@
+
+
+
diff --git a/library/layout/layout.ecf b/library/layout/layout.ecf
new file mode 100644
index 0000000..4c9e4fc
--- /dev/null
+++ b/library/layout/layout.ecf
@@ -0,0 +1,3 @@
+
+
+
diff --git a/library/license.lic b/library/license.lic
new file mode 100644
index 0000000..3e50931
--- /dev/null
+++ b/library/license.lic
@@ -0,0 +1,3 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR}, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
diff --git a/library/model/cms_model-safe.ecf b/library/model/cms_model-safe.ecf
new file mode 100644
index 0000000..db1d7ee
--- /dev/null
+++ b/library/model/cms_model-safe.ecf
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/model/cms_model.ecf b/library/model/cms_model.ecf
new file mode 100644
index 0000000..5e20c98
--- /dev/null
+++ b/library/model/cms_model.ecf
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/model/license.lic b/library/model/license.lic
new file mode 100644
index 0000000..1f97942
--- /dev/null
+++ b/library/model/license.lic
@@ -0,0 +1,3 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR}, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
diff --git a/library/model/src/link/cms_external_link.e b/library/model/src/link/cms_external_link.e
new file mode 100644
index 0000000..b133a11
--- /dev/null
+++ b/library/model/src/link/cms_external_link.e
@@ -0,0 +1,58 @@
+note
+ description: "[
+ Abstraction to represent a link outside current CMS system.
+ For instance, a web url.
+ ]"
+ date: "$Date$"
+ revision: "$Revision: 95883 $"
+
+class
+ CMS_EXTERNAL_LINK
+
+inherit
+ CMS_LINK
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_title: like title; a_location: like location)
+ -- Create current external link with optional title `a_title' and location `a_location'.
+ do
+ title := a_title
+ location := a_location
+ end
+
+feature -- Status report
+
+ is_active: BOOLEAN = False
+ --
+
+ is_expanded: BOOLEAN = False
+ --
+
+ is_collapsed: BOOLEAN = False
+ --
+
+ is_expandable: BOOLEAN = False
+ --
+
+ has_children: BOOLEAN = False
+ --
+
+feature -- Security
+
+ is_forbidden: BOOLEAN = False
+ --
+
+feature -- Access
+
+ children: detachable LIST [CMS_LINK]
+ --
+
+invariant
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/link/cms_link.e b/library/model/src/link/cms_link.e
new file mode 100644
index 0000000..4f981a8
--- /dev/null
+++ b/library/model/src/link/cms_link.e
@@ -0,0 +1,139 @@
+note
+ description: "[
+ Abstraction to represent a URI link in the CMS system.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+deferred class
+ CMS_LINK
+
+inherit
+ REFACTORING_HELPER
+ undefine
+ is_equal
+ end
+
+ DEBUG_OUTPUT
+ undefine
+ is_equal
+ end
+
+ ITERABLE [CMS_LINK]
+ undefine
+ is_equal
+ end
+
+ COMPARABLE
+
+feature -- Access
+
+ title: READABLE_STRING_32
+ -- Associated title.
+
+ location: READABLE_STRING_8
+ -- Associated url location.
+
+ weight: INTEGER
+ -- Optional weight used for order.
+
+feature -- Comparison
+
+ is_less alias "<" (other: like Current): BOOLEAN
+ -- Is current object less than `other'?
+ do
+ if weight = other.weight then
+ Result := title < other.title
+ else
+ Result := weight < other.weight
+ end
+ end
+
+feature -- Status report
+
+ is_active: BOOLEAN
+ -- Is current link active?
+ -- i.e: related to requested url.
+ deferred
+ end
+
+ is_expanded: BOOLEAN
+ -- Is expanded and visually expanded?
+ deferred
+ end
+
+ is_collapsed: BOOLEAN
+ -- Is expanded, but visually collapsed?
+ deferred
+ ensure
+ Result implies is_expandable
+ end
+
+ is_expandable: BOOLEAN
+ -- Is expandable?
+ deferred
+ end
+
+ has_children: BOOLEAN
+ -- Has sub link?
+ deferred
+ end
+
+feature -- Security
+
+ is_forbidden: BOOLEAN
+ -- Is Current link forbidden?
+ -- Current link could be disabled for current CMS user.
+ deferred
+ end
+
+feature -- Element change
+
+ set_weight (a_weight: INTEGER)
+ -- Set `weight' to `a_weight'.
+ do
+ weight := a_weight
+ ensure
+ weight_set: weight = a_weight
+ end
+
+feature -- Query
+
+ parent: detachable CMS_LINK
+ -- Optional parent link.
+
+ children: detachable LIST [CMS_LINK]
+ -- Optional children links.
+ -- Useful to have a non flat menu.
+ deferred
+ end
+
+feature -- Access
+
+ new_cursor: ITERATION_CURSOR [CMS_LINK]
+ -- Fresh cursor associated with current structure
+ do
+ if attached children as lst then
+ Result := lst.new_cursor
+ else
+ Result := (create {ARRAYED_LIST [CMS_LINK]}.make (0)).new_cursor
+ end
+ end
+
+feature -- Status report
+
+ debug_output: STRING_32
+ -- String that should be displayed in debugger to represent `Current'.
+ do
+ create Result.make_from_string (title)
+ Result.append_string_general (" -> ")
+ Result.append_string_general (location)
+ Result.append_string_general (" [")
+ Result.append_integer (weight)
+ Result.append_string_general ("]")
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/link/cms_link_composite.e b/library/model/src/link/cms_link_composite.e
new file mode 100644
index 0000000..2c45636
--- /dev/null
+++ b/library/model/src/link/cms_link_composite.e
@@ -0,0 +1,73 @@
+note
+ description: "[
+ Abstraction to represent a links container in the CMS system.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+deferred class
+ CMS_LINK_COMPOSITE
+
+inherit
+ ITERABLE [CMS_LINK]
+
+feature -- Access
+
+ items: detachable LIST [CMS_LINK]
+ -- Children links.
+ deferred
+ end
+
+feature -- Element change
+
+ extend (lnk: CMS_LINK)
+ -- Add `lnk' as a sub link.
+ deferred
+ end
+
+ remove (lnk: CMS_LINK)
+ -- Remove link `lnk' from Current container.
+ deferred
+ end
+
+ sort
+ -- Sort `items' and also recursively in sub items.
+ local
+ l_sorter: QUICK_SORTER [CMS_LINK]
+ do
+ create l_sorter.make (create {COMPARABLE_COMPARATOR [CMS_LINK]})
+ if attached items as lst then
+ l_sorter.sort (lst)
+ across
+ lst as ic
+ loop
+ if
+ attached {CMS_LINK_COMPOSITE} ic.item as l_composite and then
+ not l_composite.is_empty
+ then
+ l_composite.sort
+ end
+ end
+ end
+ end
+
+feature -- status report
+
+ is_empty: BOOLEAN
+ -- Is container empty?
+ do
+ Result := not attached items as l_items or else l_items.is_empty
+ end
+
+ count: INTEGER
+ -- Number of immediate sub links.
+ do
+ if attached items as l_items then
+ Result := l_items.count
+ end
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/link/cms_local_link.e b/library/model/src/link/cms_local_link.e
new file mode 100644
index 0000000..09fa2fc
--- /dev/null
+++ b/library/model/src/link/cms_local_link.e
@@ -0,0 +1,199 @@
+note
+ description: "[
+ Abstraction to represent a link to a CMS resource.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_LOCAL_LINK
+
+inherit
+ CMS_LINK
+
+ CMS_LINK_COMPOSITE
+ rename
+ items as children,
+ extend as add_link,
+ remove as remove_link
+ undefine
+ is_equal
+ end
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_title: detachable READABLE_STRING_GENERAL; a_location: like location)
+ -- Create current local link with optional title `a_title' and location `a_location'.
+ require
+ is_valid_local_location_argument: not a_location.starts_with_general ("/")
+ do
+ location := a_location
+ set_title (a_title)
+ end
+
+feature -- Access
+
+ permission_arguments: detachable ITERABLE [READABLE_STRING_8]
+ -- Permissions required by current link resource.
+
+ children: detachable LIST [CMS_LINK]
+ --
+
+feature -- Status report
+
+ is_active: BOOLEAN
+ --
+
+ is_expanded: BOOLEAN
+ --
+ do
+ Result := is_expandable and then internal_is_expanded
+ end
+
+ is_collapsed: BOOLEAN
+ --
+ do
+ Result := is_expandable and then internal_is_collapsed
+ end
+
+ is_expandable: BOOLEAN
+ --
+ do
+ Result := internal_is_expandable or internal_is_expanded or has_children
+ end
+
+ has_children: BOOLEAN
+ --
+ do
+ Result := attached children as l_children and then not l_children.is_empty
+ end
+
+feature -- Security
+
+ is_forbidden: BOOLEAN
+ --
+ -- Related to `permission_arguments' values.
+
+feature -- Element change
+
+ set_title (a_title: detachable READABLE_STRING_GENERAL)
+ -- Set `title' to `a_title' or `location'.
+ do
+ if a_title /= Void then
+ title := a_title.as_string_32
+ else
+ title := location.as_string_32
+ end
+ end
+
+ add_link (lnk: CMS_LINK)
+ --
+ local
+ lst: like children
+ do
+ lst := children
+ if lst = Void then
+ create {ARRAYED_LIST [CMS_LINK]} lst.make (1)
+ children := lst
+ end
+ lst.force (lnk)
+ end
+
+ remove_link (lnk: CMS_LINK)
+ --
+ local
+ lst: like children
+ do
+ lst := children
+ if lst /= Void then
+ lst.prune_all (lnk)
+ if lst.is_empty then
+ children := Void
+ end
+ end
+ end
+
+ set_children (lst: like children)
+ -- Set `children' to `lst'.
+ do
+ children := lst
+ ensure
+ children_set: children = lst
+ end
+
+ set_permission_arguments (args: like permission_arguments)
+ -- Set `permission_arguments' to `args'.
+ do
+ permission_arguments := args
+ ensure
+ permission_arguments_set: permission_arguments = args
+ end
+
+feature -- Status change
+
+ set_is_active (b: BOOLEAN)
+ -- Set `is_active' to `b'.
+ do
+ is_active := b
+ ensure
+ is_active: is_active = b
+ end
+
+ set_expanded (b: like is_expanded)
+ -- Set `is_expanded' to `b'.
+ do
+ if b then
+ set_expandable (True)
+ set_collapsed (False)
+ end
+ internal_is_expanded := b
+ ensure
+ is_expanded: is_expanded = b
+ end
+
+ set_collapsed (b: like is_collapsed)
+ -- Set `is_collapsed' to `b'.
+ do
+ if b then
+ set_expanded (False)
+ end
+ internal_is_collapsed := b
+ ensure
+ is_collapsed: is_collapsed= b
+ end
+
+ set_expandable (b: like is_expandable)
+ -- Set `is_expandable' to `b'.
+ do
+ internal_is_expandable := b
+ ensure
+ is_expandable: is_expandable = b
+ end
+
+feature -- Security change
+
+ set_is_forbidden (b: BOOLEAN)
+ -- Set `is_forbidden' to `b'.
+ do
+ is_forbidden := b
+ ensure
+ is_forbidden: is_forbidden = b
+ end
+
+feature {NONE} -- Implementation
+
+ internal_is_expandable: BOOLEAN
+
+ internal_is_expanded: BOOLEAN
+
+ internal_is_collapsed: BOOLEAN
+
+invariant
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/link/cms_menu.e b/library/model/src/link/cms_menu.e
new file mode 100644
index 0000000..6f3a292
--- /dev/null
+++ b/library/model/src/link/cms_menu.e
@@ -0,0 +1,109 @@
+note
+ description: "[
+ Abstraction to represent a MENU in the CMS system.
+ It has items as sub menu/link.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_MENU
+
+inherit
+ CMS_LINK_COMPOSITE
+ redefine
+ is_empty
+ end
+
+create
+ make,
+ make_with_title
+
+feature {NONE} -- Initialization
+
+ make (a_name: like name; a_capacity: INTEGER)
+ -- Create menu with name `a_name' and a capacity of `a_capacity'.
+ do
+ name := a_name
+ create items.make (a_capacity)
+ ensure
+ name_set: name = a_name
+ items_set: items.capacity = a_capacity
+ end
+
+ make_with_title (a_name: like name; a_title: READABLE_STRING_32; a_capacity: INTEGER)
+ -- Create menu with name `a_name' and a capacity of `a_capacity', and title `a_title'
+ do
+ make (a_name, a_capacity)
+ set_title (a_title)
+ ensure
+ name_set: name = a_name
+ title_set: title = a_title
+ items_set: items.capacity = a_capacity
+ end
+
+feature -- Access
+
+ name: READABLE_STRING_8
+ -- Identifier for Current menu.
+
+ title: detachable READABLE_STRING_32
+ -- Optional title.
+
+ items: ARRAYED_LIST [CMS_LINK]
+ --
+
+feature -- Status report
+
+ is_empty: BOOLEAN
+ --
+ do
+ Result := items.is_empty
+ end
+
+ has (lnk: CMS_LINK): BOOLEAN
+ -- Has the current Menu a link `lnk'.
+ do
+ across
+ items as ic
+ until
+ Result
+ loop
+ Result := ic.item.location.same_string (lnk.location)
+ end
+ end
+
+feature -- Element change
+
+ extend (lnk: CMS_LINK)
+ --
+ do
+ items.extend (lnk)
+ end
+
+ remove (lnk: CMS_LINK)
+ --
+ do
+ items.prune_all (lnk)
+ end
+
+ set_title (t: like title)
+ -- Set `title' with `t'.
+ do
+ title := t
+ end
+
+feature -- Access
+
+ new_cursor: ITERATION_CURSOR [CMS_LINK]
+ -- Fresh cursor associated with current structure
+ do
+ Result := items.new_cursor
+ end
+
+invariant
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/log/cms_log.e b/library/model/src/log/cms_log.e
new file mode 100644
index 0000000..c50d932
--- /dev/null
+++ b/library/model/src/log/cms_log.e
@@ -0,0 +1,127 @@
+note
+ description: "Summary description for {CMS_LOG}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_LOG
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_category: like category; a_message: like message; a_level: like level; a_date: detachable like date)
+ do
+ category := a_category
+ message := a_message
+ set_level (a_level)
+ if a_date = Void then
+ create date.make_now_utc
+ else
+ date := a_date
+ end
+ end
+
+ make_with_id (a_id: like id; a_category: like category; a_message: like message; a_level: like level; a_date: detachable like date)
+ do
+ id := a_id
+ make (a_category, a_message, a_level, a_date)
+ end
+
+feature -- Access
+
+ id: INTEGER
+ -- Unique identifier of Current.
+
+ category: READABLE_STRING_8
+ -- Associated title (optional).
+
+ message: READABLE_STRING_8
+ -- Log message
+
+ level: INTEGER
+ -- Severity level
+
+ level_name: STRING
+ do
+ Result := level_to_string (level)
+ end
+
+ info: detachable READABLE_STRING_8
+
+ link: detachable CMS_LINK
+
+ date: DATE_TIME
+
+feature -- status report
+
+ has_id: BOOLEAN
+ do
+ Result := id > 0
+ end
+
+feature -- Change
+
+ set_id (a_id: like id)
+ require
+ not has_id
+ do
+ id := a_id
+ end
+
+ set_level (a_level: like level)
+ do
+ if a_level = 0 then
+ level := level_notice
+ else
+ level := a_level
+ end
+ end
+
+ set_link (lnk: like link)
+ do
+ link := lnk
+ end
+
+ set_info (inf: like info)
+ do
+ info := inf
+ end
+
+feature -- Constants
+
+ level_to_string (a_level: INTEGER): STRING
+ do
+ inspect a_level
+ when level_emergency then
+ Result := "emergency"
+ when level_alert then
+ Result := "alert"
+ when level_critical then
+ Result := "critical"
+ when level_error then
+ Result := "error"
+ when level_warning then
+ Result := "warning"
+ when level_notice then
+ Result := "notice"
+ when level_info then
+ Result := "info"
+ when level_debug then
+ Result := "debug"
+ else
+ Result := "level-" + a_level.out
+ end
+ end
+
+ level_emergency: INTEGER = 1
+ level_alert: INTEGER = 2
+ level_critical: INTEGER = 3
+ level_error: INTEGER = 4
+ level_warning: INTEGER = 5
+ level_notice: INTEGER = 6
+ level_info: INTEGER = 7
+ level_debug: INTEGER = 8
+
+end
diff --git a/library/model/src/user/cms_partial_user.e b/library/model/src/user/cms_partial_user.e
new file mode 100644
index 0000000..c2a7e32
--- /dev/null
+++ b/library/model/src/user/cms_partial_user.e
@@ -0,0 +1,19 @@
+note
+ description: "Partial CMS USER."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PARTIAL_USER
+
+inherit
+ CMS_USER
+
+create
+ make,
+ make_with_id -- MAYBE: export to CMS_STORAGE
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/user/cms_user.e b/library/model/src/user/cms_user.e
new file mode 100644
index 0000000..b0dfbec
--- /dev/null
+++ b/library/model/src/user/cms_user.e
@@ -0,0 +1,307 @@
+note
+ description: "[
+ Interface representing a USER in the CMS system.
+ ]"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ CMS_USER
+
+inherit
+
+ DEBUG_OUTPUT
+
+create
+ make,
+ make_with_id -- MAYBE: export to CMS_STORAGE
+
+feature {NONE} -- Initialization
+
+ make (a_name: READABLE_STRING_32)
+ -- Create an object with name `a_name'.
+ require
+ a_name_not_empty: not a_name.is_whitespace
+ do
+ name := a_name
+ initialize
+ ensure
+ name_set: name = a_name
+ status_not_active: status = not_active
+ end
+
+ make_with_id (a_id: INTEGER_64)
+ require
+ a_id_valid: a_id > 0
+ do
+ id := a_id
+ name := {STRING_32} ""
+ initialize
+ ensure
+ id_set: id = a_id
+ status_not_active: status = not_active
+ end
+
+ initialize
+ do
+ create creation_date.make_now_utc
+ mark_not_active
+ end
+
+feature -- Access
+
+ id: INTEGER_64
+ -- Unique id.
+
+ name: READABLE_STRING_32
+ -- User name.
+
+ password: detachable READABLE_STRING_32
+ -- User password (plain text password).
+
+ hashed_password: detachable READABLE_STRING_8
+ -- Hashed user password.
+
+ email: detachable READABLE_STRING_32
+ -- User email.
+
+ profile: detachable CMS_USER_PROFILE
+ -- User profile.
+
+ creation_date: DATE_TIME
+ -- Creation date.
+
+ last_login_date: detachable DATE_TIME
+ -- User last login.
+
+ status: INTEGER
+ -- Associated status for the current user.
+ -- default: not_active
+ -- active
+ -- trashed
+
+
+feature -- Access: helper
+
+ utf_8_name: STRING_8
+ -- UTF-8 version of `name'.
+ local
+ utf: UTF_CONVERTER
+ do
+ Result := utf.utf_32_string_to_utf_8_string_8 (name)
+ end
+
+feature -- Roles
+
+ roles: detachable LIST [CMS_USER_ROLE]
+ -- If set, list of roles for current user.
+
+feature -- Access: data
+
+ data: detachable STRING_TABLE [detachable ANY]
+ -- Additional data.
+
+ data_item (k: READABLE_STRING_GENERAL): detachable ANY
+ -- Additional item data associated with key `k'.
+ do
+ if attached data as l_data then
+ Result := l_data.item (k)
+ end
+ end
+
+feature -- Status report
+
+ has_id: BOOLEAN
+ do
+ Result := id > 0
+ end
+
+ has_email: BOOLEAN
+ do
+ Result := attached email as e and then not e.is_empty
+ end
+
+ debug_output: STRING_32
+ do
+ create Result.make_from_string (name)
+ if has_id then
+ Result.append_character (' ')
+ Result.append_character ('<')
+ Result.append_integer_64 (id)
+ Result.append_character ('>')
+ end
+ end
+
+ same_as (other: detachable CMS_USER): BOOLEAN
+ -- Is Current same as `other'?
+ do
+ Result := other /= Void and then id = other.id
+ end
+
+ is_active: BOOLEAN
+ -- is the current user active?
+ do
+ Result := status = {CMS_USER}.active
+ end
+
+feature -- Change element
+
+ set_id (a_id: like id)
+ -- Set `id' with `a_id'.
+ do
+ id := a_id
+ ensure
+ id_set: id = a_id
+ end
+
+ set_name (n: like name)
+ -- Set `name' with `n'.
+ do
+ name := n
+ ensure
+ name_set: name = n
+ end
+
+ set_password (a_password: like password)
+ -- Set `password' with `a_password'.
+ do
+ password := a_password
+ hashed_password := Void
+ ensure
+ password_set: password = a_password
+ hashed_password_void: hashed_password = Void
+ end
+
+ set_hashed_password (a_hashed_password: like hashed_password)
+ -- Set `hashed_password' with `a_hashed_password'.
+ do
+ hashed_password := a_hashed_password
+ password := Void
+ ensure
+ password_void: password = Void
+ hashed_password_set: hashed_password = a_hashed_password
+ end
+
+ set_email (a_email: like email)
+ -- Set `email' with `a_email'.
+ do
+ email := a_email
+ ensure
+ email_set: email = a_email
+ end
+
+ set_profile (prof: like profile)
+ -- Set `profile' with `prof'.
+ do
+ profile := prof
+ ensure
+ profile_set: profile = prof
+ end
+
+ set_profile_item (k: READABLE_STRING_8; v: READABLE_STRING_8)
+ local
+ prof: like profile
+ do
+ prof := profile
+ if prof = Void then
+ create prof.make
+ profile := prof
+ end
+ prof.force (v, k)
+ end
+
+ set_last_login_date (dt: like last_login_date)
+ do
+ last_login_date := dt
+ end
+
+ set_last_login_date_now
+ do
+ set_last_login_date (create {DATE_TIME}.make_now_utc)
+ end
+
+feature -- Element change: roles
+
+ set_roles (lst: like roles)
+ -- Set `roles' to `lst'.
+ do
+ roles := lst
+ end
+
+feature -- Change element: data
+
+ set_data_item (k: READABLE_STRING_GENERAL; d: like data_item)
+ -- Associate data item `d' with key `k'.
+ local
+ l_data: like data
+ do
+ l_data := data
+ if l_data = Void then
+ create l_data.make (1)
+ data := l_data
+ end
+ l_data.force (d, k)
+ end
+
+ remove_data_item (k: READABLE_STRING_GENERAL)
+ -- Remove data item associated with key `k'.
+ do
+ if attached data as l_data then
+ l_data.remove (k)
+ end
+ end
+
+feature -- Status change
+
+ mark_not_active
+ -- Set status to not_active
+ do
+ set_status (not_active)
+ ensure
+ status_not_active: status = not_active
+ end
+
+ mark_active
+ -- Set status to active.
+ do
+ set_status (active)
+ ensure
+ status_active: status = active
+ end
+
+ mark_trashed
+ -- Set status to trashed.
+ do
+ set_status (trashed)
+ ensure
+ status_trash: status = trashed
+ end
+
+ set_status (a_status: like status)
+ -- Assign `status' with `a_status'.
+ do
+ status := a_status
+ ensure
+ status_set: status = a_status
+ end
+
+
+feature -- User status
+
+ not_active: INTEGER = 0
+ -- The user is not active.
+
+ active: INTEGER = 1
+ -- The user is active
+
+ Trashed: INTEGER = -1
+ -- The user is trashed (soft delete), ready to be deleted/destroyed from storage.
+
+invariant
+
+ id_or_name_set: id > 0 or else not name.is_whitespace
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/user/cms_user_profile.e b/library/model/src/user/cms_user_profile.e
new file mode 100644
index 0000000..1f0a407
--- /dev/null
+++ b/library/model/src/user/cms_user_profile.e
@@ -0,0 +1,56 @@
+note
+ description: "[
+ User profile used to extend information associated with a {CMS_USER}.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_USER_PROFILE
+
+inherit
+ TABLE_ITERABLE [READABLE_STRING_8, READABLE_STRING_GENERAL]
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create Current profile.
+ do
+ create items.make (0)
+ end
+
+feature -- Access
+
+ item (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_8
+ -- Profile item associated with key `k'.
+ do
+ Result := items.item (k)
+ end
+
+feature -- Change
+
+ force (v: READABLE_STRING_8; k: READABLE_STRING_GENERAL)
+ -- Associated value `v' with key `k'.
+ do
+ items.force (v, k)
+ end
+
+feature -- Access
+
+ new_cursor: TABLE_ITERATION_CURSOR [READABLE_STRING_8, READABLE_STRING_GENERAL]
+ -- Fresh cursor associated with current structure
+ do
+ Result := items.new_cursor
+ end
+
+feature {NONE} -- Implementation
+
+ items: STRING_TABLE [READABLE_STRING_8]
+
+;note
+ copyright: "2011-2014, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/model/src/user/cms_user_role.e b/library/model/src/user/cms_user_role.e
new file mode 100644
index 0000000..e44c08f
--- /dev/null
+++ b/library/model/src/user/cms_user_role.e
@@ -0,0 +1,130 @@
+note
+ description: "[
+ The user roles are used for the CMS permission system.
+ A role has a list of permissions, that describes what users of current role
+ are authorized to operation.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_USER_ROLE
+
+inherit
+ ANY
+ redefine
+ is_equal
+ end
+
+create
+ make,
+ make_with_id
+
+feature {NONE} -- Initialization
+
+ make_with_id (a_id: like id)
+ -- Create current role with role id `a_id'.
+ do
+ id := a_id
+ name := {STRING_32} ""
+ initialize
+ end
+
+ make (a_name: like name)
+ -- Create current role with name `a_name'.
+ require
+ a_name_valid: not a_name.is_whitespace
+ do
+ name := a_name
+ initialize
+ end
+
+ initialize
+ do
+ create {ARRAYED_LIST [READABLE_STRING_8]} permissions.make (0)
+ end
+
+feature -- Status report
+
+ has_id: BOOLEAN
+ -- Has a unique identifier?
+ do
+ Result := id > 0
+ end
+
+ has_permission (p: READABLE_STRING_GENERAL): BOOLEAN
+ -- Has permission `p'?
+ do
+ Result := across permissions as c some p.is_case_insensitive_equal (c.item) end
+ end
+
+feature -- Access
+
+ id: INTEGER
+ -- Unique id associated with Current role.
+
+ name: READABLE_STRING_32
+ -- Name of Current role.
+
+ permissions: LIST [READABLE_STRING_8]
+ -- List of permissions.
+
+feature -- Comparison
+
+ same_user_role (other: CMS_USER_ROLE): BOOLEAN
+ -- Is Current role same as role `other' ?
+ do
+ Result := other.id = id
+ end
+
+ is_equal (other: like Current): BOOLEAN
+ -- Is `other' attached to an object considered
+ -- equal to current object?
+ do
+ Result := id = other.id
+ end
+
+feature -- Change
+
+ set_id (a_id: like id)
+ -- Set `id' with `a_id'.
+ do
+ id := a_id
+ ensure
+ set_id: id = a_id
+ end
+
+ set_name (a_name: like name)
+ -- Set `name' with `a_name'.
+ do
+ name := a_name
+ ensure
+ name_set: name = a_name
+ end
+
+feature -- Permission change
+
+ add_permission (a_permission_name: READABLE_STRING_8)
+ -- Add permission `a_permission_name' to Current role.
+ require
+ is_not_blank: not a_permission_name.is_whitespace
+ do
+ permissions.force (a_permission_name)
+ ensure
+ has_permission: has_permission (a_permission_name)
+ end
+
+ remove_permission (a_permission_name: READABLE_STRING_8)
+ -- Remove permission `a_permission_name' from Current role.
+ require
+ is_not_blank: not a_permission_name.is_whitespace
+ do
+ permissions.prune_all (a_permission_name)
+ ensure
+ not_has_permission: not has_permission (a_permission_name)
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/library/persistence/Readme.md b/library/persistence/Readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/library/persistence/implementation/store/cms_storage_store_sql.e b/library/persistence/implementation/store/cms_storage_store_sql.e
new file mode 100644
index 0000000..aec4408
--- /dev/null
+++ b/library/persistence/implementation/store/cms_storage_store_sql.e
@@ -0,0 +1,191 @@
+note
+ description: "Storage based on Eiffel Store component."
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+deferred class
+ CMS_STORAGE_STORE_SQL
+
+inherit
+ CMS_STORAGE_SQL
+
+feature {NONE} -- Initialization
+
+ make (a_connection: DATABASE_CONNECTION)
+ --
+ require
+ is_connected: a_connection.is_connected
+ do
+ connection := a_connection
+ write_information_log (generator + ".make - is database connected? "+ a_connection.is_connected.out )
+
+ create {DATABASE_HANDLER_IMPL} db_handler.make (a_connection)
+
+ create error_handler.make
+ end
+
+feature -- Status report
+
+ is_available: BOOLEAN
+ -- Is storage available?
+ do
+ Result := connection.is_connected
+ end
+
+feature -- Basic operation
+
+ close
+ --
+ -- Disconnect from SQL database.
+ do
+ connection.disconnect
+ end
+
+feature {NONE} -- Implementation
+
+ db_handler: DATABASE_HANDLER
+
+ connection: DATABASE_CONNECTION
+ -- Current database connection.
+
+feature -- Query
+
+ sql_post_execution
+ -- Post database execution.
+ do
+ error_handler.append (db_handler.database_error_handler)
+ if error_handler.has_error then
+ write_critical_log (generator + ".post_execution " + error_handler.as_string_representation)
+ end
+ end
+
+ sql_begin_transaction
+ --
+ do
+ connection.begin_transaction
+ end
+
+ sql_rollback_transaction
+ --
+ do
+ connection.rollback
+ end
+
+ sql_commit_transaction
+ --
+ do
+ connection.commit
+ end
+
+ sql_query (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ -- Execute an sql query `a_sql_statement' with the params `a_params'.
+ do
+ check_sql_query_validity (a_sql_statement, a_params)
+ db_handler.set_query (create {DATABASE_QUERY}.data_reader (a_sql_statement, a_params))
+ db_handler.execute_query
+ sql_post_execution
+ end
+
+ sql_finalize
+ --
+ do
+ -- N/A
+ end
+
+ sql_insert (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ --
+ do
+ check_sql_query_validity (a_sql_statement, a_params)
+ db_handler.set_query (create {DATABASE_QUERY}.data_reader (a_sql_statement, a_params))
+ db_handler.execute_change
+ sql_post_execution
+ end
+
+ sql_modify (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ --
+ do
+ check_sql_query_validity (a_sql_statement, a_params)
+ db_handler.set_query (create {DATABASE_QUERY}.data_reader (a_sql_statement, a_params))
+ db_handler.execute_change
+ sql_post_execution
+ end
+
+ sql_rows_count: INTEGER
+ -- Number of rows for last sql execution.
+ do
+ Result := db_handler.count
+ end
+
+ sql_start
+ -- Set the cursor on first element.
+ do
+ db_handler.start
+ end
+
+ sql_after: BOOLEAN
+ -- Are there no more items to iterate over?
+ do
+ Result := db_handler.after
+ end
+
+ sql_forth
+ -- Fetch next row from last sql execution, if any.
+ do
+ db_handler.forth
+ end
+
+ sql_valid_item_index (a_index: INTEGER): BOOLEAN
+ do
+ Result := attached {DB_TUPLE} db_handler.item as l_item and then l_item.valid_index (a_index)
+ end
+
+ sql_item (a_index: INTEGER): detachable ANY
+ do
+ if attached {DB_TUPLE} db_handler.item as l_item and then l_item.count >= a_index then
+ Result := l_item.item (a_index)
+ else
+ check has_item_at_index: False end
+ end
+ end
+
+ sql_read_integer_32 (a_index: INTEGER): INTEGER_32
+ -- Retrieved value at `a_index' position in `item'.
+ local
+ l_item: like sql_item
+ i64: INTEGER_64
+ do
+ l_item := sql_item (a_index)
+ if attached {INTEGER_32} l_item as i then
+ Result := i
+ elseif attached {INTEGER_32_REF} l_item as l_value then
+ Result := l_value.item
+ else
+ if attached {INTEGER_64} l_item as i then
+ i64 := i
+ elseif attached {INTEGER_64_REF} l_item as l_value then
+ i64 := l_value.item
+ else
+ check is_integer_32: False end
+ end
+ if i64 <= {INTEGER_32}.max_value then
+ Result := i64.to_integer_32
+ else
+ check is_integer_32: False end
+ end
+ end
+ end
+
+ sql_read_date_time (a_index: INTEGER): detachable DATE_TIME
+ -- Retrieved value at `a_index' position in `item'.
+ local
+ l_item: like sql_item
+ do
+ l_item := sql_item (a_index)
+ if attached {DATE_TIME} l_item as dt then
+ Result := dt
+ else
+ check is_date_time_or_null: l_item = Void end
+ end
+ end
+
+end
diff --git a/library/persistence/implementation/store/cms_storage_store_sql_builder.e b/library/persistence/implementation/store/cms_storage_store_sql_builder.e
new file mode 100644
index 0000000..6fc4b15
--- /dev/null
+++ b/library/persistence/implementation/store/cms_storage_store_sql_builder.e
@@ -0,0 +1,16 @@
+note
+ description: "[
+ Common ancestor for builders responsible to instantiate storage based
+ on Eiffel Store storage.
+ ]"
+ author: "$Author: jfiat $"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+deferred class
+ CMS_STORAGE_STORE_SQL_BUILDER
+
+inherit
+ CMS_STORAGE_SQL_BUILDER
+
+end
diff --git a/library/persistence/implementation/store/database/database_config.e b/library/persistence/implementation/store/database/database_config.e
new file mode 100644
index 0000000..64ff6f5
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_config.e
@@ -0,0 +1,29 @@
+note
+ description: "Database configuration"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+deferred class
+ DATABASE_CONFIG
+
+feature -- Database access
+
+ default_hostname: STRING = ""
+ -- Database hostname.
+
+ default_username: STRING = ""
+ -- Database username.
+
+ default_password: STRING = ""
+ -- Database password.
+
+ default_database_name: STRING = "EiffelDB"
+ -- Database name.
+
+ is_keep_connection: BOOLEAN
+ -- Keep Connection to database?
+ do
+ Result := True
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/database_connection.e b/library/persistence/implementation/store/database/database_connection.e
new file mode 100644
index 0000000..0d3d826
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_connection.e
@@ -0,0 +1,191 @@
+note
+ description: "Abstract class to handle a database connection"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+deferred class
+ DATABASE_CONNECTION
+
+inherit
+
+ DATABASE_CONFIG
+
+feature {NONE} -- Initialization
+
+ login (a_username: STRING; a_password: STRING; a_hostname: STRING; a_database_name: STRING; connection: BOOLEAN)
+
+ -- Create a database handler with user `a_username', password `a_password',
+ -- host `a_hostname', database_name `a_database_name', and keep_connection `connection'.
+ require
+ username_not_void: a_username /= Void
+ username_not_empty: not a_username.is_empty
+ password_not_void: a_password /= Void
+ hostname_not_void: a_hostname /= Void
+ hotname_not_empty: not a_hostname.is_empty
+ database_name_not_void: a_database_name /= Void
+ database_name_not_empty: not a_database_name.is_empty
+ deferred
+ ensure
+ db_application_not_void: db_application /= Void
+ db_control_not_void: db_control /= Void
+ end
+
+ login_with_connection_string (a_connection_string: STRING)
+ -- Login with `a_connection_string'
+ -- and immediately connect to database.
+ deferred
+ ensure
+ db_application_not_void: db_application /= Void
+ db_control_not_void: db_control /= Void
+ end
+
+ login_with_default
+ -- Create a database handler with common settings.
+ deferred
+ ensure
+ db_application_not_void: db_application /= Void
+ db_control_not_void: db_control /= Void
+ end
+
+ login_with_database_name ( a_database_name: STRING)
+ -- Create a database handler with common settings and
+ -- set database_name with `a_database_name'.
+ require
+ database_name_not_void: a_database_name /= Void
+ database_name_not_empty: not a_database_name.is_empty
+ deferred
+ ensure
+ db_application_not_void: db_application /= Void
+ db_control_not_void: db_control /= Void
+ end
+
+feature -- Database Setup
+
+ db_application: DATABASE_APPL [DATABASE]
+ -- Database application.
+
+ db_control: DB_CONTROL
+ -- Database control.
+
+ keep_connection: BOOLEAN
+ -- Keep connection alive?
+
+feature -- Transactions
+
+ begin_transaction
+ -- Start a transaction which will be terminated by a call to `rollback' or `commit'.
+ local
+ rescued: BOOLEAN
+ do
+ if not rescued then
+ if db_control.is_ok then
+ db_control.begin
+ else
+ database_error_handler.add_database_error (db_control.error_message_32, db_control.error_code)
+ end
+ end
+ rescue
+ rescued := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ db_control.reset
+ retry
+ end
+
+ commit
+ -- Commit updates in the database.
+ local
+ rescued: BOOLEAN
+ do
+ if not rescued then
+ if db_control.is_ok then
+ db_control.commit
+ else
+ database_error_handler.add_database_error (db_control.error_message_32, db_control.error_code)
+ end
+ end
+ rescue
+ rescued := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ db_control.reset
+ retry
+ end
+
+ rollback
+ -- Rollback updates in the database.
+ local
+ rescued: BOOLEAN
+ do
+ if not rescued then
+ if db_control.is_ok then
+ db_control.rollback
+ else
+ database_error_handler.add_database_error (db_control.error_message_32, db_control.error_code)
+ end
+ end
+ rescue
+ rescued := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ db_control.reset
+ retry
+ end
+
+feature -- Change Element
+
+ not_keep_connection
+ do
+ keep_connection := False
+ end
+
+feature -- Conection
+
+ connect
+ -- Connect to the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ if not is_connected then
+ db_control.connect
+ end
+ end
+
+ disconnect
+ -- Disconnect from the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ db_control.disconnect
+ end
+
+ is_connected: BOOLEAN
+ -- True if connected to the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ Result := db_control.is_connected
+ end
+
+feature -- Error Handling
+
+ database_error_handler: DATABASE_ERROR_HANDLER
+ -- Error handler.
+
+feature -- Status Report
+
+ has_error: BOOLEAN
+ -- Has error?
+ do
+ Result := database_error_handler.has_error
+ end
+
+feature -- Helper
+
+ exception_as_error (a_exception: like {EXCEPTION_MANAGER}.last_exception)
+ -- Record exception `a_exception' as an error.
+ do
+ if a_exception /= Void and then attached a_exception.trace as l_trace then
+ database_error_handler.add_error_details (a_exception.code, once "Exception", l_trace)
+ end
+ end
+
+
+end
diff --git a/library/persistence/implementation/store/database/database_connection_null.e b/library/persistence/implementation/store/database/database_connection_null.e
new file mode 100644
index 0000000..06a3ed9
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_connection_null.e
@@ -0,0 +1,68 @@
+note
+ description: "Null object to meet Void Safe."
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_CONNECTION_NULL
+
+inherit
+
+ DATABASE_CONNECTION
+ redefine
+ db_application,
+ is_connected
+ end
+
+create
+ login, login_with_default, login_with_database_name
+
+feature -- Initialization
+
+ login_with_default
+ -- Create a database handler for ODBC with common settings.
+ do
+ create database_error_handler.make
+ create db_application.login (default_username, default_password)
+ db_application.set_hostname (default_username)
+ db_application.set_data_source (default_database_name)
+ db_application.set_base
+ create db_control.make
+ end
+
+ login (a_username: STRING; a_password: STRING; a_hostname: STRING; a_database_name: STRING; connection: BOOLEAN)
+
+ -- Create a database handler for ODBC.
+ do
+ login_with_default
+ end
+
+ login_with_database_name (a_database_name: STRING)
+
+ -- Create a database handler for ODBC.
+ do
+ login_with_default
+ end
+
+
+ login_with_connection_string (a_string: STRING)
+ -- Login with `a_connection_string'
+ -- and immediately connect to database.
+ do
+ login_with_default
+ end
+
+
+feature -- Databse Connection
+
+ db_application: DATABASE_APPL[DATABASE_NULL]
+ -- Database application.
+
+
+ is_connected: BOOLEAN
+ -- True if connected to the database.
+ do
+ Result := True
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/database_connection_odbc.e b/library/persistence/implementation/store/database/database_connection_odbc.e
new file mode 100644
index 0000000..b74f547
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_connection_odbc.e
@@ -0,0 +1,108 @@
+note
+ description: "Object that handle a database connection for ODBC"
+ date: "$Date: 2014-11-13 12:23:47 -0300 (ju., 13 nov. 2014) $"
+ revision: "$Revision: 96085 $"
+
+class
+ DATABASE_CONNECTION_ODBC
+
+inherit
+ DATABASE_CONNECTION
+ redefine
+ db_application
+ end
+
+create
+ login, login_with_default, login_with_database_name, login_with_connection_string
+
+feature -- Initialization
+
+ login (a_username: STRING; a_password: STRING; a_hostname: STRING; a_database_name: STRING; a_keep_connection: BOOLEAN)
+
+ -- Create a database handler for ODBC and set `username' to `a_username',
+ -- `password' to `a_password'
+ -- `database_name' to `a_database_name'
+ -- `keep_connection' to `a_keep_connection'
+ local
+ retried: BOOLEAN
+ l_database_error_handler: detachable like database_error_handler
+ do
+ create l_database_error_handler.make
+ database_error_handler := l_database_error_handler
+ create db_application.login (a_username, a_password)
+ if not retried then
+ db_application.set_hostname (a_hostname)
+ db_application.set_data_source (a_database_name)
+ db_application.set_base
+ create db_control.make
+ keep_connection := a_keep_connection
+ if keep_connection then
+ connect
+ end
+ else
+ create db_control.make
+ if is_connected then
+ disconnect
+ end
+ end
+ rescue
+ if l_database_error_handler = Void then
+ create l_database_error_handler.make
+ end
+ database_error_handler := l_database_error_handler
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ retried := True
+ retry
+ end
+
+ login_with_default
+ -- Create a database handler for ODBC with common settings.
+ do
+ login_with_database_name (default_database_name)
+ end
+
+ login_with_database_name (a_database_name: STRING)
+ -- Create a database handler and
+ -- set database_name to `a_database_name'.
+ do
+ login (default_username, default_password, default_hostname, default_database_name, is_keep_connection)
+ end
+
+ login_with_connection_string (a_string: STRING)
+ -- Login with `a_connection_string'and immediately connect to database.
+ local
+ retried: BOOLEAN
+ l_database_error_handler: detachable like database_error_handler
+ do
+ create l_database_error_handler.make
+ database_error_handler := l_database_error_handler
+ create db_application.login_with_connection_string (a_string)
+ if not retried then
+ db_application.set_base
+ create db_control.make
+ keep_connection := is_keep_connection
+ if keep_connection then
+ connect
+ end
+ else
+ create db_control.make
+ if is_connected then
+ disconnect
+ end
+ end
+ rescue
+ if l_database_error_handler = Void then
+ create l_database_error_handler.make
+ end
+ database_error_handler := l_database_error_handler
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ retried := True
+ retry
+ end
+
+feature -- Databse Connection
+
+ db_application: DATABASE_APPL [ODBC]
+ -- Database application.
+
+end
diff --git a/library/persistence/implementation/store/database/database_handler.e b/library/persistence/implementation/store/database/database_handler.e
new file mode 100644
index 0000000..c974b35
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_handler.e
@@ -0,0 +1,237 @@
+note
+ description: "Abstract Database Handler"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+deferred class
+ DATABASE_HANDLER
+
+inherit
+
+ SHARED_LOGGER
+
+feature -- Access
+
+ store: detachable DATABASE_STORE_PROCEDURE
+ -- Database stored_procedure to handle.
+
+ query: detachable DATABASE_QUERY
+ -- Database query.
+
+feature -- Modifiers
+
+ set_store (a_store: DATABASE_STORE_PROCEDURE)
+ -- Set `store' to `a_store' to execute.
+ require
+ store_not_void: a_store /= Void
+ do
+ store := a_store
+ ensure
+ store_set: store = a_store
+ end
+
+ set_query (a_query: DATABASE_QUERY)
+ -- Set `query' to `a_query' to execute.
+ require
+ query_not_void: a_query /= Void
+ do
+ query := a_query
+ ensure
+ query_set: query = a_query
+ end
+
+feature -- Functionality Store Procedures
+
+ execute_store_reader
+ -- Execute a `store' to read data.
+ require
+ store_not_void: store /= Void
+ deferred
+ end
+
+ execute_store_writer
+ -- Execute a `store' to write data.
+ require
+ store_not_void: store /= Void
+ deferred
+ end
+
+feature -- SQL Queries
+
+ execute_query
+ -- Execute sql query, the read data from the database.
+ require
+ query_not_void: query /= Void
+ deferred
+ end
+
+ execute_change
+ -- Execute sql query that update/add data.
+ require
+ query_not_void: query /= Void
+ deferred
+ end
+
+
+feature -- Iteration
+
+ start
+ -- Set the cursor on first element.
+ deferred
+ end
+
+ item: ANY
+ -- Item at current cursor position.
+ require
+ valid_position: not after
+ deferred
+ end
+
+ after: BOOLEAN
+ -- Are there no more items to iterate over?
+ deferred
+ end
+
+ forth
+ -- Move to next position.
+ require
+ valid_position: not after
+ deferred
+ end
+
+feature -- Access
+
+ read_integer_32 (a_index: INTEGER): INTEGER_32
+ -- Retrieved value at `a_index' position in `item'.
+ do
+ if attached {DB_TUPLE} item as l_item then
+ if attached {INTEGER_32_REF} l_item.item (a_index) as ll_item then
+ Result := ll_item.item
+ end
+ end
+ end
+
+ read_string (a_index: INTEGER): detachable STRING
+ -- Retrieved value at `a_index' position in `item'.
+ do
+ if attached {DB_TUPLE} item as l_item then
+ if attached {STRING} l_item.item (a_index) as ll_item then
+ Result := ll_item
+ elseif attached {BOOLEAN_REF} l_item.item (a_index) as ll_item then
+ Result := ll_item.item.out
+ end
+ end
+ end
+
+ read_date_time (a_index: INTEGER): detachable DATE_TIME
+ -- Retrieved value at `a_index' position in `item'.
+ do
+ if attached {DB_TUPLE} item as l_item then
+ if attached {DATE_TIME} l_item.item (a_index) as ll_item then
+ Result := ll_item
+ end
+ end
+ end
+
+ read_boolean (a_index: INTEGER): detachable BOOLEAN
+ -- Retrieved value at `a_index' position in `item'.
+ do
+ if attached {DB_TUPLE} item as l_item then
+ if attached {BOOLEAN} l_item.item (a_index) as ll_item then
+ Result := ll_item
+ elseif attached {BOOLEAN_REF} l_item.item (a_index) as ll_item then
+ Result := ll_item.item
+ end
+ end
+ end
+
+feature -- Status Report
+
+ count: INTEGER
+ -- Number of rows, last execution.
+ deferred
+ end
+
+ connection: DATABASE_CONNECTION
+ -- Database connection.
+
+ db_control: DB_CONTROL
+ -- Database control.
+ do
+ Result := connection.db_control
+ end
+
+ db_result: detachable DB_RESULT
+ -- Database query result.
+
+ db_selection: detachable DB_SELECTION
+ -- Database selection.
+
+ db_change: detachable DB_CHANGE
+ -- Database modification.
+
+feature -- Error handling
+
+ check_database_change_error
+ -- Check database error from `db_change'.
+ do
+ if attached db_change as l_change and then not l_change.is_ok then
+ database_error_handler.add_database_error (l_change.error_message_32, l_change.error_code)
+ write_error_log (generator + ".check_database_change_error: " + l_change.error_message_32)
+ l_change.reset
+ end
+ end
+
+ check_database_selection_error
+ -- Check database error from `db_selection'.
+ do
+ if attached db_selection as l_selection and then not l_selection.is_ok then
+ database_error_handler.add_database_error (l_selection.error_message_32, l_selection.error_code)
+ write_error_log (generator + ".check_database_selection_error: " + l_selection.error_message_32)
+ l_selection.reset
+ end
+ end
+
+feature -- Error Handling
+
+ database_error_handler: DATABASE_ERROR_HANDLER
+ -- Error handler.
+
+feature -- Status Report
+
+ has_error: BOOLEAN
+ -- Has error?
+ do
+ Result := database_error_handler.has_error
+ end
+
+feature -- Helper
+
+ exception_as_error (a_e: like {EXCEPTION_MANAGER}.last_exception)
+ -- Record exception as an error.
+ do
+ if attached a_e as l_e and then attached l_e.trace as l_trace then
+ database_error_handler.add_error_details (l_e.code, once "Exception", l_trace.as_string_32)
+ end
+ end
+
+feature -- Connection Handling
+
+ connect
+ -- Connect to the database.
+ deferred
+ end
+
+ disconnect
+ -- Disconnect from the database.
+ deferred
+ ensure
+ not_connected: not is_connected
+ end
+
+ is_connected: BOOLEAN
+ -- True if connected to the database.
+ deferred
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/database_handler_impl.e b/library/persistence/implementation/store/database/database_handler_impl.e
new file mode 100644
index 0000000..e6e61b6
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_handler_impl.e
@@ -0,0 +1,234 @@
+note
+ description: "Database handler Implementation"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_HANDLER_IMPL
+
+inherit
+ DATABASE_HANDLER
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_connection: DATABASE_CONNECTION)
+ -- Create a database handler with connnection `connection'.
+ do
+ connection := a_connection
+ create last_query.make_now
+ create database_error_handler.make
+ ensure
+ connection_not_void: connection /= Void
+ last_query_not_void: last_query /= Void
+ database_error_handler_set: attached database_error_handler
+ end
+
+feature -- Functionality
+
+ execute_store_reader
+ -- Execute stored procedure that returns data.
+ local
+ l_db_selection: DB_SELECTION
+ l_retried: BOOLEAN
+ do
+ if not l_retried then
+ database_error_handler.reset
+ if attached store as l_store then
+ create l_db_selection.make
+ db_selection := l_db_selection
+ items := l_store.execute_reader (l_db_selection)
+ check_database_selection_error
+ end
+ write_debug_log ( generator+".execute_reader Successful")
+ end
+ rescue
+ l_retried := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ if attached db_selection as l_selection then
+ l_selection.reset
+ end
+ retry
+ end
+
+ execute_store_writer
+ -- Execute stored procedure that update/add data.
+ local
+ l_db_change: DB_CHANGE
+ l_retried : BOOLEAN
+ do
+ if not l_retried then
+ database_error_handler.reset
+ if attached store as l_store then
+ create l_db_change.make
+ db_change := l_db_change
+ l_store.execute_writer (l_db_change)
+ check_database_change_error
+ end
+ write_debug_log ( generator+".execute_writer Successful")
+ end
+ rescue
+ l_retried := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ if attached db_change as l_change then
+ l_change.reset
+ end
+ retry
+ end
+
+feature -- SQL Queries
+
+ execute_query
+ -- Execute query.
+ local
+ l_db_selection: DB_SELECTION
+ l_retried: BOOLEAN
+ do
+ if not l_retried then
+ database_error_handler.reset
+ if attached query as l_query then
+ create l_db_selection.make
+ db_selection := l_db_selection
+ items := l_query.execute_reader (l_db_selection)
+ check_database_selection_error
+ end
+ end
+ rescue
+ l_retried := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ if attached db_selection as l_selection then
+ l_selection.reset
+ end
+ retry
+ end
+
+
+ execute_change
+ -- Execute sql_query that update/add data.
+ local
+ l_db_change: DB_CHANGE
+ l_retried : BOOLEAN
+ do
+ if not l_retried then
+ database_error_handler.reset
+ if attached query as l_query then
+ create l_db_change.make
+ db_change := l_db_change
+ l_query.execute_change (l_db_change)
+ check_database_change_error
+ end
+ end
+ rescue
+ l_retried := True
+ exception_as_error ((create {EXCEPTION_MANAGER}).last_exception)
+ if attached db_change as l_change then
+ l_change.reset
+ end
+ retry
+ end
+
+
+feature -- Iteration
+
+ start
+ -- Set the cursor on first element.
+ do
+ if attached db_selection as l_db_selection and then l_db_selection.container /= Void then
+ l_db_selection.start
+ end
+ end
+
+ forth
+ -- Move cursor to next element.
+ do
+ if attached db_selection as l_db_selection then
+ l_db_selection.forth
+ else
+ check False end
+ end
+ end
+
+ after: BOOLEAN
+ -- True for the last element.
+ do
+ if attached db_selection as l_db_selection and then l_db_selection.container /= Void then
+ Result := l_db_selection.after or else l_db_selection.cursor = Void
+ else
+ Result := True
+ end
+ end
+
+ item: DB_TUPLE
+ -- Current element.
+ do
+ if attached db_selection as l_db_selection and then attached l_db_selection.cursor as l_cursor then
+ create {DB_TUPLE} Result.copy (l_cursor)
+ else
+ check False then end
+ end
+ end
+
+
+feature {NONE} -- Implementation
+
+ last_query: DATE_TIME
+ -- Last time when a query was executed.
+
+ keep_connection: BOOLEAN
+ -- Keep connection alive?
+ do
+ Result := connection.keep_connection
+ end
+
+ connect
+ -- Connect to the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ if not is_connected then
+ db_control.connect
+ end
+ end
+
+ disconnect
+ -- Disconnect from the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ db_control.disconnect
+ end
+
+ is_connected: BOOLEAN
+ -- True if connected to the database.
+ require else
+ db_control_not_void: db_control /= Void
+ do
+ Result := db_control.is_connected
+ end
+
+ affected_row_count: INTEGER
+ -- The number of rows changed, deleted, or inserted by the last statement.
+ do
+ if attached db_change as l_update then
+ Result := l_update.affected_row_count
+ end
+ end
+
+feature -- Result
+
+ items : detachable LIST[DB_RESULT]
+ -- Query result.
+
+ count: INTEGER
+ --
+ do
+ if attached items as l_items then
+ Result := l_items.count
+ end
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/database_iteration_cursor.e b/library/persistence/implementation/store/database/database_iteration_cursor.e
new file mode 100644
index 0000000..1688b76
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_iteration_cursor.e
@@ -0,0 +1,91 @@
+note
+ description: "External iteration cursor for {DATABASE_HANDLER}"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_ITERATION_CURSOR [G]
+
+inherit
+
+ ITERATION_CURSOR [G]
+
+ ITERABLE [G]
+
+create
+ make
+
+feature -- Initialization
+
+ make (a_handler: DATABASE_HANDLER; a_action: like action)
+ -- Create an iterator and set `db_handlet' to `a_handler'
+ -- `action' to `a_action'
+ do
+ db_handler := a_handler
+ action := a_action
+ ensure
+ db_handler_set: db_handler = a_handler
+ action_set: action = a_action
+ end
+
+feature -- Access
+
+ item: G
+ -- Item at current cursor position.
+ do
+ Result := action.item ([db_item])
+ end
+
+ db_item: DB_TUPLE
+ -- Current element.
+ do
+ if attached {DB_TUPLE} db_handler.item as l_item then
+ Result := l_item
+ else
+ check False then
+ end
+ end
+ end
+
+feature -- Status report
+
+ after: BOOLEAN
+ -- Are there no more items to iterate over?
+ do
+ Result := db_handler.after
+ end
+
+feature -- Cursor movement
+
+ start
+ -- Set the cursor on first element.
+ do
+ db_handler.start
+ end
+
+ forth
+ -- Move to next position.
+ do
+ db_handler.forth
+ end
+
+feature -- Cursor
+
+ new_cursor: DATABASE_ITERATION_CURSOR [G]
+ --
+ do
+ Result := twin
+ Result.start
+ end
+
+feature -- Action
+
+ action: FUNCTION [ANY, detachable TUPLE, G]
+ -- Agent to create a new item of type G.
+
+feature {NONE} -- Implementation
+
+ db_handler: DATABASE_HANDLER
+ -- Associated handler used for iteration.
+
+end
diff --git a/library/persistence/implementation/store/database/database_null.e b/library/persistence/implementation/store/database/database_null.e
new file mode 100644
index 0000000..a26437a
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_null.e
@@ -0,0 +1,539 @@
+note
+ description: "Null object to meet void safe"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_NULL
+
+inherit
+ DATABASE
+
+
+feature -- Acccess
+
+ database_handle_name: STRING = "NULL"
+ -- Handle name
+
+feature -- For DATABASE_STATUS
+
+ is_error_updated: BOOLEAN
+ -- Has an NULL function been called since last update which may have
+ -- updated error code, error message?
+
+ is_warning_updated: BOOLEAN
+ -- Has an ODBC function been called since last update which may have
+ -- updated warning message?
+
+ found: BOOLEAN
+ -- Is there any record matching the last
+ -- selection condition used ?
+
+ clear_error
+ -- Reset database error status.
+ do
+ end
+
+ insert_auto_identity_column: BOOLEAN = False
+ -- For INSERTs and UPDATEs should table auto-increment identity columns be explicitly included in the statement?
+
+feature -- For DATABASE_CHANGE
+
+ descriptor_is_available: BOOLEAN
+ do
+ end
+
+
+feature -- For DATABASE_FORMAT
+
+ date_to_str (object: DATE_TIME): STRING
+ -- String representation in SQL of `object'
+ -- For ODBC, ORACLE
+ do
+ create Result.make_empty
+ end
+
+ string_format (object: detachable STRING): STRING
+ -- String representation in SQL of `object'
+ obsolete
+ "Use `string_format_32' instead."
+ do
+ Result := ""
+ end
+
+ string_format_32 (object: detachable READABLE_STRING_GENERAL): STRING_32
+ -- String representation in SQL of `object'
+ do
+ Result := ""
+ end
+
+ True_representation: STRING
+ -- Database representation of the boolean True
+ do
+ Result := ""
+ end
+
+ False_representation: STRING
+ -- Database representation of the boolean False
+ do
+ Result := ""
+ end
+
+feature -- For DATABASE_SELECTION, DATABASE_CHANGE
+
+ normal_parse: BOOLEAN
+ -- Should the SQL string be normal parsed,
+ -- using SQL_SCAN?
+ do
+ end
+
+feature -- DATABASE_STRING
+
+ sql_name_string: STRING
+ -- SQL type name of string
+ do
+ Result := ""
+ end
+
+feature -- DATABASE_REAL
+
+ sql_name_real: STRING
+ -- SQL type name for real
+ do
+ Result := ""
+ end
+
+feature -- DATABASE_DATETIME
+
+ sql_name_datetime: STRING
+ -- SQL type name for datetime
+ do
+ Result := ""
+ end
+
+feature -- DATABASE_DECIMAL
+
+ sql_name_decimal: STRING
+ -- SQL type name for decimal
+ do
+ Result := ""
+ end
+
+feature -- DATABASE_DOUBLE
+
+ sql_name_double: STRING
+ -- SQL type name for double
+ do
+ Result := ""
+ end
+
+feature -- DATABASE_CHARACTER
+
+ sql_name_character: STRING
+ -- SQL type name for character
+ do
+ Result := ""
+ end
+feature -- DATABASE_INTEGER
+
+ sql_name_integer: STRING
+ -- SQL type name for integer
+ do
+ Result := ""
+ end
+
+ sql_name_integer_16: STRING
+ -- SQL type name for integer
+ do
+ Result := ""
+ end
+
+ sql_name_integer_64: STRING
+ -- SQL type name for integer
+ do
+ Result := ""
+ end
+feature -- DATABASE_BOOLEAN
+
+ sql_name_boolean: STRING
+ -- SQL type name for boolean
+ do
+ Result := ""
+ end
+
+feature -- LOGIN and DATABASE_APPL only for password_ok
+
+
+ password_ok (upasswd: STRING): BOOLEAN
+ -- Can the user password be Void?
+ do
+ Result := True
+ end
+
+ password_ensure (name, passwd, uname, upasswd: STRING): BOOLEAN
+ -- Is name equal to uname and passwd equal to upasswd?
+ do
+ end
+
+feature -- For DATABASE_PROC
+
+
+ support_sql_of_proc: BOOLEAN
+ -- Does the database support SQL attachment to the stored procedure?
+ do
+ end
+
+ support_stored_proc: BOOLEAN
+ -- Does the database support creating a stored procedure?
+ do
+ end
+
+ sql_as: STRING
+ -- Creating a stored procedure "as"...
+ do
+ Result := ""
+ end
+
+ sql_end: STRING
+ -- End of the stored procedure creation string.
+ do
+ Result := ""
+ end
+
+ sql_execution: STRING
+ -- Begining of the stored procedure execution string.
+ do
+ Result := ""
+ end
+
+ sql_creation: STRING
+ -- Begining of the stored procedure creation string.
+ do
+ Result := ""
+ end
+
+ sql_after_exec: STRING
+ -- End of the stored procedure execution string.
+ do
+ Result := ""
+ end
+
+ support_drop_proc: BOOLEAN
+ -- Does the database support stored procedure dropping from server?
+ do
+ end
+
+ name_proc_lower: BOOLEAN
+ -- Has the name of the stored procedure to be in lower case?
+ do
+ end
+
+ map_var_between: STRING
+ -- @ symbol for ODBC and Sybase
+ do
+ Result := ""
+ end
+
+ map_var_name_32 (par_name: READABLE_STRING_GENERAL): STRING_32
+ -- Redefined for Sybase
+ do
+ Result := ""
+ end
+
+ Select_text_32 (proc_name: READABLE_STRING_GENERAL): STRING_32
+ -- SQL query to get stored procedure text
+ do
+ Result := ""
+ end
+
+ Select_exists_32 (name: READABLE_STRING_GENERAL): STRING_32
+ -- SQL query to test stored procedure existing
+ do
+ Result := ""
+ end
+
+
+ Selection_string (rep_qualifier, rep_owner, repository_name: STRING): STRING
+ -- String to select the table needed
+ do
+ Result := ""
+ end
+
+ sql_string: STRING
+ -- Database type of a string
+ -- with a size less than Max_char_size
+ do
+ Result := ""
+ end
+
+ sql_string2 (int: INTEGER): STRING
+ -- Database type of a string
+ -- with a size more than Max_char_size
+ do
+ Result := ""
+ end
+
+ sql_wstring: STRING
+ -- Database type of a string
+ -- with a size less than Max_char_size
+ do
+ Result := ""
+ end
+
+ sql_wstring2 (int: INTEGER): STRING
+ -- Database type of a string
+ -- with a size more than Max_char_size
+ do
+ Result := ""
+ end
+
+feature -- External features
+
+ get_error_message: POINTER
+ -- Function related with the error processing
+ do
+ create Result
+ end
+
+ get_error_message_string: STRING_32
+ -- Function related with the error processing
+ do
+ Result := ""
+ end
+
+ get_error_code: INTEGER
+ -- Function related with the error processing
+ do
+ end
+
+ get_warn_message: POINTER
+ -- Function related with the error processing
+ do
+ create Result
+ end
+
+ get_warn_message_string: STRING_32
+ -- Function related with the error processing
+ do
+ Result := ""
+ end
+
+ new_descriptor: INTEGER
+ -- A descriptor is used to store a row fetched by FETCH command
+ -- Whenever perform a SELECT statement, allocate a new descriptor
+ -- by int_new_descriptor(), the descriptor is freed
+ -- when the SELECT statement terminates.
+ do
+ end
+
+ init_order (no_descriptor: INTEGER; command: READABLE_STRING_GENERAL)
+ -- In DYNAMICALLY EXECUTE mode perform the SQL statement
+ -- But this routine only get things ready for dynamic execution:
+ -- 1. get the SQL statement PREPAREd; and check if there are
+ -- warning message for the SQL statement;
+ -- 2. DESCRIBE the SQL statement and get enough information to
+ -- allocate enough memory space for the corresponding descriptor.
+ do
+ end
+
+ start_order (no_descriptor: INTEGER)
+ -- Finish execution of a SQL statement in DYNAMICLLY EXECUTION mode: */
+ -- 1. if the PREPAREd SQL statement is a NON_SELECT statement,
+ -- just EXECUTE it; otherwise, DEFINE a CURSOR for it and
+ -- OPEN the CURSOR. In the process, if error occurs, do some
+ -- clearence;
+ do
+ end
+
+ next_row (no_descriptor: INTEGER)
+ -- A SELECT statement is now being executed in DYNAMIC EXECUTION mode,
+ -- the routine is to FETCH a new tuple from database
+ -- and if a new tuple is fetched, return 1 otherwise return 0.
+ do
+ end
+
+ terminate_order (no_descriptor: INTEGER)
+ -- A SQL has been performed in DYNAMIC EXECUTION mode,
+ -- so the routine is to do some clearence:
+ -- 1. if the DYNAMICALLY EXECUTED SQL statement is a NON_SELECT
+ -- statement, just free the memory for ODBCSQLDA and clear
+ -- the cell in 'descriptor' to NULL; otherwise, CLOSE the CURSOR
+ -- and then do the same clearence.
+ -- 2. return error number.
+ do
+ end
+
+ close_cursor (no_descriptor: INTEGER)
+ -- A SQL has been performed in DYNAMIC EXECUTION mode,
+ -- Then if the DYNAMICALLY EXECUTED SQL statement is a SELECT
+ -- statement, then the cursor is closed.
+ -- Then one can do an other selection on the previous cursor.
+ do
+ end
+
+ exec_immediate (no_descriptor: INTEGER; command: READABLE_STRING_GENERAL)
+ -- In IMMEDIATE EXECUTE mode perform the SQL statement,
+ -- and then check if there is warning message for the execution,
+ do
+ end
+
+ put_col_name (no_descriptor: INTEGER; index: INTEGER; ar: STRING; max_len:INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ put_data (no_descriptor: INTEGER; index: INTEGER; ar: STRING; max_len:INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ put_data_32 (no_descriptor: INTEGER; index: INTEGER; ar: STRING_32; max_len:INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+
+ conv_type (indicator: INTEGER; index: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ --| FIXME
+ --| This description really does not explain a thing...
+ do
+ end
+
+ get_count (no_descriptor: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_data_len (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_col_len (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_col_type (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_integer_data (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_integer_16_data (no_descriptor: INTEGER; ind: INTEGER): INTEGER_16
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_integer_64_data (no_descriptor: INTEGER; ind: INTEGER): INTEGER_64
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_float_data (no_descriptor: INTEGER; ind: INTEGER): DOUBLE
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_real_data (no_descriptor: INTEGER; ind: INTEGER): REAL
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_boolean_data (no_descriptor: INTEGER; ind: INTEGER): BOOLEAN
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ is_null_data (no_descriptor: INTEGER; ind: INTEGER): BOOLEAN
+ -- Is last retrieved data null?
+ do
+ end
+
+ get_date_data (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_hour (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_sec (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_min (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_year (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_day (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_month (no_descriptor: INTEGER; ind: INTEGER): INTEGER
+ -- Function used to get data from structure SQLDA filled by FETCH clause.
+ do
+ end
+
+ get_decimal (no_descriptor: INTEGER; ind: INTEGER): detachable TUPLE [digits: STRING_8; sign, precision, scale: INTEGER]
+ -- Function used to get decimal info
+ do
+ end
+
+
+ database_make (i: INTEGER)
+ -- Initialize database c-module
+ do
+ end
+
+ connect (user_name, user_passwd, data_source, application, hostname, role_id: STRING; role_passwd: detachable STRING; group_id: STRING)
+ -- Connect to database
+ do
+ end
+
+ connect_by_connection_string (a_connect_string: STRING)
+ -- Connect to database by connection string
+ do
+ end
+
+ disconnect
+ -- Disconnect the current connection with an database
+ do
+ end
+
+ commit
+ -- Commit the current transaction
+ do
+ end
+
+ rollback
+ -- Commit the current transaction
+ do
+ end
+
+ trancount: INTEGER
+ -- Return the number of transactions now active
+ do
+ end
+
+ begin
+ -- Begin a data base transaction
+ do
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/database_query.e b/library/persistence/implementation/store/database/database_query.e
new file mode 100644
index 0000000..531409d
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_query.e
@@ -0,0 +1,124 @@
+note
+ description: "Abstract Database Query"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ DATABASE_QUERY
+
+inherit
+ REFACTORING_HELPER
+
+ SHARED_LOGGER
+
+create
+ data_reader
+
+feature {NONE} -- Intialization
+
+ data_reader (a_query: STRING; a_parameters: like parameters)
+ -- SQL data reader for the query `a_query' with arguments `a_parameters'
+ do
+ write_information_log (generator + ".data_reader" + " execute query: " + a_query)
+ write_debug_log (generator + ".data_reader" + " arguments:" + log_parameters (a_parameters))
+ query := a_query
+ parameters := a_parameters
+ ensure
+ query_set: query = a_query
+ parameters_set: parameters = a_parameters
+ end
+
+feature -- Execution
+
+ execute_reader (a_base_selection: DB_SELECTION): detachable LIST [DB_RESULT]
+ -- Execute the Current sql query.
+ do
+ to_implement ("Check test dynamic sequel. to redesign.")
+ create {ARRAYED_LIST [DB_RESULT]} Result.make (100)
+ a_base_selection.set_container (Result)
+ set_map_name (a_base_selection)
+ a_base_selection.set_query (query)
+ a_base_selection.execute_query
+ if a_base_selection.is_ok then
+ a_base_selection.load_result
+ Result := a_base_selection.container
+ else
+ write_error_log (generator + "." + a_base_selection.error_message_32)
+ end
+ unset_map_name (a_base_selection)
+ a_base_selection.terminate
+ end
+
+ execute_change (a_base_change: DB_CHANGE)
+ -- Execute the Current sql query to change/update data in the database.
+ do
+ to_implement ("Check test dynamic sequel. to redesign.")
+ set_map_name (a_base_change)
+ a_base_change.set_query (query)
+ a_base_change.execute_query
+ unset_map_name (a_base_change)
+ end
+
+feature -- Access
+
+ query: STRING
+ -- SQL query to execute.
+
+ parameters: detachable STRING_TABLE [detachable ANY]
+ -- query parameters.
+
+feature {NONE} -- Implementation
+
+ set_map_name (a_base_selection: DB_EXPRESSION)
+ -- Store parameters `item' and their `key'.
+ do
+ if attached parameters as l_parameters then
+ across
+ l_parameters as ic
+ loop
+ a_base_selection.set_map_name (ic.item, ic.key)
+ end
+ end
+ end
+
+ unset_map_name (a_base_selection: DB_EXPRESSION)
+ -- Remove parameters item associated with key `key'.
+ do
+ if attached parameters as l_parameters then
+ across
+ l_parameters as ic
+ loop
+ a_base_selection.unset_map_name (ic.key)
+ end
+ end
+ end
+
+ log_parameters (a_parameters: like parameters): STRING
+ -- Parameters to log with name and value
+ -- exclude sensitive information.
+ do
+ create Result.make_empty
+ if a_parameters /= Void then
+ across
+ a_parameters as ic
+ loop
+ Result.append ("name:")
+ Result.append (ic.key.as_string_32)
+ Result.append (", value:")
+ if
+ ic.key.has_substring ("Password") or else
+ ic.key.has_substring ("password")
+ then
+ -- Data to exclude
+ else
+ if attached ic.item as l_item then
+ Result.append (l_item.out)
+ end
+ end
+ Result.append ("%N")
+ end
+ end
+ end
+
+
+end -- DATABASE_QUERY
diff --git a/library/persistence/implementation/store/database/database_sql_server_encoder.e b/library/persistence/implementation/store/database/database_sql_server_encoder.e
new file mode 100644
index 0000000..d33c057
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_sql_server_encoder.e
@@ -0,0 +1,34 @@
+note
+ description: "Help to encode sql queries, to prevent sql injections."
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ EIS: "SQL server injection", "src=http://blogs.msdn.com/b/raulga/archive/2007/01/04/dynamic-sql-sql-injection.aspx", "protocol=url"
+
+expanded class
+ DATABASE_SQL_SERVER_ENCODER
+
+inherit
+
+ SHARED_LOGGER
+
+feature -- Escape SQL input
+
+ encode (a_string: READABLE_STRING_32): READABLE_STRING_32
+ -- Escape single quote (') and braces ([,]).
+ local
+ l_string: STRING
+ do
+ l_string := a_string.twin
+ if not l_string.is_empty then
+ l_string.replace_substring_all ("[", "[[")
+ l_string.replace_substring_all ("]", "]]")
+ if l_string.index_of ('%'', 1) > 0 then
+ l_string.replace_substring ("[", 1, l_string.index_of ('%'', 1))
+ end
+ if l_string.last_index_of ('%'', l_string.count) > 0 then
+ l_string.replace_substring ("]", l_string.last_index_of ('%'', l_string.count), l_string.count)
+ end
+ end
+ Result := l_string
+ end
+end
diff --git a/library/persistence/implementation/store/database/database_store_procedure.e b/library/persistence/implementation/store/database/database_store_procedure.e
new file mode 100644
index 0000000..97d5ecd
--- /dev/null
+++ b/library/persistence/implementation/store/database/database_store_procedure.e
@@ -0,0 +1,192 @@
+note
+ description: "Database Store Procedure"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_STORE_PROCEDURE
+
+inherit
+
+ SHARED_ERROR
+
+create
+ data_reader, data_writer
+
+feature -- Intialization
+
+ data_reader (a_sp: STRING; a_parameters: HASH_TABLE [ANY, STRING_32])
+ -- SQL data reader for the stored procedure `a_sp' with arguments `a_parameters'.
+ local
+ l_retried: BOOLEAN
+ do
+ write_information_log (generator + ".data_reader" + " execute store procedure: " + a_sp)
+ write_debug_log (generator + ".data_reader" + " arguments:" + log_parameters (a_parameters))
+ if not l_retried then
+ stored_procedure := a_sp
+ parameters := a_parameters
+ create proc.make (stored_procedure)
+ proc.load
+ if not a_parameters.is_empty then
+ proc.set_arguments_32 (a_parameters.current_keys, a_parameters.linear_representation.to_array)
+ end
+ if proc.exists then
+ if proc.text_32 /= Void then
+ debug
+ write_debug_log ( generator + ".data_reader: " + proc.text_32)
+ end
+ end
+ else
+ has_error := True
+ error_message := proc.error_message_32
+ error_code := proc.error_code
+ write_error_log (generator + ".data_witer message:" + proc.error_message_32 + " code:" + proc.error_code.out)
+ end
+ else
+ stored_procedure := a_sp
+ parameters := a_parameters
+ create proc.make (stored_procedure)
+ end
+ rescue
+ set_last_error_from_exception ("SQL execution")
+ write_critical_log (generator+ ".data_reader " + last_error_message)
+ l_retried := True
+ retry
+ end
+
+ data_writer (a_sp: STRING; a_parameters: HASH_TABLE [ANY, STRING_32])
+ -- SQL data reader for the stored procedure `a_sp' with arguments `a_parameters'
+ local
+ l_retried: BOOLEAN
+ do
+ write_information_log (generator + ".data_reader" + " execute store procedure: " + a_sp)
+ write_debug_log (generator + ".data_reader" + " arguments:" + log_parameters (a_parameters))
+ if not l_retried then
+ stored_procedure := a_sp
+ parameters := a_parameters
+ create proc.make (stored_procedure)
+ proc.load
+ proc.set_arguments_32 (a_parameters.current_keys, a_parameters.linear_representation.to_array)
+ if proc.exists then
+ if proc.text_32 /= Void then
+ debug
+ write_debug_log ( generator + ".data_writer: " + proc.text_32)
+ end
+ end
+ else
+ has_error := True
+ error_message := proc.error_message_32
+ error_code := proc.error_code
+ write_error_log (generator + ".data_witer message:" + proc.error_message_32 + " code:" + proc.error_code.out)
+ end
+ else
+ stored_procedure := a_sp
+ parameters := a_parameters
+ create proc.make (stored_procedure)
+ end
+ rescue
+ set_last_error_from_exception ("SQL execution")
+ write_critical_log (generator+ ".data_reader " + last_error_message)
+ l_retried := True
+ retry
+ end
+
+ execute_reader (a_base_selection: DB_SELECTION): detachable LIST [DB_RESULT]
+ -- Execute the Current store procedure.
+ do
+ create {ARRAYED_LIST [DB_RESULT]} Result.make (100)
+ a_base_selection.set_container (Result)
+ set_map_name (a_base_selection)
+ proc.execute (a_base_selection)
+ a_base_selection.load_result
+ Result := a_base_selection.container
+ unset_map_name (a_base_selection)
+ end
+
+ execute_writer (a_base_change: DB_CHANGE)
+ -- Execute the Current store procedure.
+ do
+ set_map_name (a_base_change)
+ proc.execute (a_base_change)
+ unset_map_name (a_base_change)
+ end
+
+feature -- Access
+
+ proc: DB_PROC
+ -- object to create and execute stored procedure.
+
+ parameters: HASH_TABLE [detachable ANY, STRING_32]
+ -- Parameters to be used by the stored procedure.
+
+ stored_procedure: STRING
+ -- Store procedure to execute
+
+feature -- Status Report
+
+ has_error: BOOLEAN
+ -- Is there an error.
+
+ error_message: detachable STRING_32
+ -- Last error message.
+
+ error_code: INTEGER
+ -- Last error code.
+
+feature {NONE} -- Implementation
+
+ set_map_name (a_base_selection: DB_EXPRESSION)
+ -- Store parameters `item' and their `key'.
+ do
+ from
+ parameters.start
+ until
+ parameters.after
+ loop
+ a_base_selection.set_map_name (parameters.item_for_iteration, parameters.key_for_iteration)
+ parameters.forth
+ end
+ end
+
+ unset_map_name (a_base_selection: DB_EXPRESSION)
+ -- Remove parameters item associated with key `key'.
+ do
+ from
+ parameters.start
+ until
+ parameters.after
+ loop
+ a_base_selection.unset_map_name (parameters.key_for_iteration)
+ parameters.forth
+ end
+ end
+
+ log_parameters (a_parameters: like parameters): STRING
+ -- Parameters to log with name and value
+ -- exclude sensitive information.
+ do
+ create Result.make_empty
+ from
+ a_parameters.start
+ until
+ a_parameters.after
+ loop
+ Result.append ("name:")
+ Result.append (a_parameters.key_for_iteration)
+ Result.append (", value:")
+ if
+ a_parameters.key_for_iteration.has_substring ("Password") or else
+ a_parameters.key_for_iteration.has_substring ("password")
+ then
+ -- Data to exclude
+ else
+ if attached a_parameters.item_for_iteration as l_item then
+ Result.append (l_item.out)
+ end
+ end
+ Result.append ("%N")
+ a_parameters.forth
+ end
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/error/database_error.e b/library/persistence/implementation/store/database/error/database_error.e
new file mode 100644
index 0000000..3f67be1
--- /dev/null
+++ b/library/persistence/implementation/store/database/error/database_error.e
@@ -0,0 +1,23 @@
+note
+ description: "Error from database"
+ date: "$Date: 2014-11-13 16:23:47 +0100 (jeu., 13 nov. 2014) $"
+ revision: "$Revision: 96085 $"
+
+class
+ DATABASE_ERROR
+
+inherit
+ ERROR_CUSTOM
+
+create
+ make_from_message
+
+feature {NONE} -- Init
+
+ make_from_message (a_m: like message; a_code: like code)
+ -- Create from `a_m'
+ do
+ make (a_code, once "Database Error", a_m)
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/error/database_error_handler.e b/library/persistence/implementation/store/database/error/database_error_handler.e
new file mode 100644
index 0000000..ae49e6e
--- /dev/null
+++ b/library/persistence/implementation/store/database/error/database_error_handler.e
@@ -0,0 +1,35 @@
+note
+ description: "Database error handler"
+ date: "$Date: 2014-11-13 16:23:47 +0100 (jeu., 13 nov. 2014) $"
+ revision: "$Revision: 96085 $"
+
+class
+ DATABASE_ERROR_HANDLER
+
+inherit
+ ERROR_HANDLER
+
+create
+ make
+
+feature -- Error operation
+
+ add_database_error (a_message: READABLE_STRING_32; a_code: INTEGER)
+ -- Add a database error.
+ local
+ l_error: DATABASE_ERROR
+ do
+ create l_error.make_from_message (a_message, a_code)
+ add_error (l_error)
+ end
+
+ add_database_no_change_error (a_message: READABLE_STRING_32; a_code: INTEGER)
+ -- Add a database error.
+ local
+ l_error: DATABASE_NO_CHANGE_ERROR
+ do
+ create l_error.make_from_message (a_message, a_code)
+ add_error (l_error)
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/error/database_no_change_error.e b/library/persistence/implementation/store/database/error/database_no_change_error.e
new file mode 100644
index 0000000..4fcc211
--- /dev/null
+++ b/library/persistence/implementation/store/database/error/database_no_change_error.e
@@ -0,0 +1,27 @@
+note
+ description: "Summary description for {DATABASE_NO_CHANGE_ERROR}."
+ author: ""
+ date: "$Date: 2014-11-13 16:23:47 +0100 (jeu., 13 nov. 2014) $"
+ revision: "$Revision: 96085 $"
+
+class
+ DATABASE_NO_CHANGE_ERROR
+
+inherit
+ DATABASE_ERROR
+ redefine
+ make_from_message
+ end
+
+create
+ make_from_message
+
+feature {NONE} -- Init
+
+ make_from_message (a_m: like message; a_code: like code)
+ -- Create from `a_m'
+ do
+ make (a_code, once "Database No Change Error", a_m)
+ end
+
+end
diff --git a/library/persistence/implementation/store/database/parameter_name_helper.e b/library/persistence/implementation/store/database/parameter_name_helper.e
new file mode 100644
index 0000000..3c614ee
--- /dev/null
+++ b/library/persistence/implementation/store/database/parameter_name_helper.e
@@ -0,0 +1,23 @@
+note
+ description: "Helper for paramenter Names"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+deferred class
+ PARAMETER_NAME_HELPER
+
+feature -- String
+
+ string_parameter (a_value: STRING; a_length: INTEGER): STRING
+ -- Adjust a parameter `a_value' to the lenght `a_length'.
+ require
+ valid_length: a_length > 0
+ do
+ if a_value.count <= a_length then
+ Result := a_value
+ else
+ create Result.make_from_string (a_value.substring (1, a_length))
+ end
+ end
+
+end
diff --git a/library/persistence/mysql/scripts/core.sql b/library/persistence/mysql/scripts/core.sql
new file mode 100644
index 0000000..366ccbd
--- /dev/null
+++ b/library/persistence/mysql/scripts/core.sql
@@ -0,0 +1,30 @@
+BEGIN;
+
+CREATE TABLE `logs` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `category` VARCHAR(255) NOT NULL,
+ `level` int(11) NOT NULL,
+ `uid` int(11) DEFAULT NULL,
+ `message` text NOT NULL,
+ `info` text,
+ `link` text,
+ `date` datetime NOT NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `custom_values` (
+ `type` VARCHAR(255) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `value` VARCHAR(255) NOT NULL
+);
+
+CREATE TABLE `path_aliases` (
+ `pid` int(11) NOT NULL AUTO_INCREMENT,
+ `source` varchar(255) NOT NULL,
+ `alias` varchar(255) NOT NULL,
+ `lang` varchar(12) DEFAULT NULL,
+ PRIMARY KEY (`pid`)
+);
+
+COMMIT;
+
diff --git a/library/persistence/mysql/scripts/node.sql b/library/persistence/mysql/scripts/node.sql
new file mode 100644
index 0000000..a4a053e
--- /dev/null
+++ b/library/persistence/mysql/scripts/node.sql
@@ -0,0 +1,24 @@
+BEGIN;
+
+CREATE TABLE nodes (
+ nid INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL CHECK( nid >=0),
+ revision INTEGER,
+ type TEXT NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ summary TEXT,
+ content MEDIUMTEXT NOT NULL,
+ format VARCHAR(255),
+ author INTEGER,
+ publish DATETIME,
+ created DATETIME NOT NULL,
+ changed DATETIME NOT NULL,
+ status INTEGER
+);
+
+CREATE TABLE page_nodes(
+ nid INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL CHECK( nid >=0),
+ revision INTEGER,
+ parent INTEGER
+);
+
+COMMIT;
diff --git a/library/persistence/mysql/scripts/user.sql b/library/persistence/mysql/scripts/user.sql
new file mode 100644
index 0000000..e99072a
--- /dev/null
+++ b/library/persistence/mysql/scripts/user.sql
@@ -0,0 +1,66 @@
+BEGIN;
+
+CREATE TABLE `users` (
+ `uid` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) NOT NULL,
+ `password` varchar(100) NOT NULL,
+ `salt` varchar(100) NOT NULL,
+ `email` varchar(250) NOT NULL,
+ `status` int(11) DEFAULT NULL,
+ `created` datetime NOT NULL,
+ `signed` datetime DEFAULT NULL,
+ CHECK (`uid` >= 0),
+ PRIMARY KEY (`uid`),
+ UNIQUE KEY `name` (`name`)
+);
+
+CREATE TABLE `roles` (
+ `rid` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) NOT NULL,
+ CHECK (`rid` >= 0),
+ PRIMARY KEY (`rid`),
+ UNIQUE KEY `name` (`name`)
+);
+
+
+CREATE TABLE `users_roles` (
+ `uid` int(11) NOT NULL,
+ `rid` int(11) NOT NULL,
+ CHECK (`uid` >= 0),
+ CHECK (`rid` >= 0)
+);
+
+CREATE TABLE `role_permissions` (
+ `rid` int(11) NOT NULL,
+ `permission` varchar(255) NOT NULL,
+ `module` varchar(255) DEFAULT NULL,
+ CHECK (`rid` >= 0)
+);
+
+
+CREATE TABLE `users_activations` (
+ `aid` int(11) NOT NULL AUTO_INCREMENT,
+ `token` varchar(255) NOT NULL,
+ `uid` int(11) NOT NULL,
+ `created` datetime NOT NULL,
+ CHECK (`aid` >= 0),
+ CHECK (`uid` >= 0),
+ PRIMARY KEY (`aid`),
+ UNIQUE KEY `token` (`token`)
+);
+
+
+CREATE TABLE `users_password_recovery` (
+ `aid` int(11) NOT NULL AUTO_INCREMENT,
+ `token` varchar(255) NOT NULL,
+ `uid` int(11) NOT NULL,
+ `created` datetime NOT NULL,
+ CHECK (`aid` >= 0),
+ CHECK (`uid` >= 0),
+ PRIMARY KEY (`aid`),
+ UNIQUE KEY `token` (`token`)
+);
+
+
+
+COMMIT;
\ No newline at end of file
diff --git a/library/persistence/sqlite3/sqlite3-safe.ecf b/library/persistence/sqlite3/sqlite3-safe.ecf
new file mode 100644
index 0000000..566217e
--- /dev/null
+++ b/library/persistence/sqlite3/sqlite3-safe.ecf
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /old$
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/persistence/sqlite3/src/cms_storage_sqlite3.e b/library/persistence/sqlite3/src/cms_storage_sqlite3.e
new file mode 100644
index 0000000..0bcd73a
--- /dev/null
+++ b/library/persistence/sqlite3/src/cms_storage_sqlite3.e
@@ -0,0 +1,450 @@
+note
+ description: "Summary description for {CMS_STORAGE_MYSQL}."
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_STORAGE_SQLITE3
+
+inherit
+ CMS_STORAGE_SQL
+ redefine
+ sql_read_date_time, sql_read_integer_32,
+ sql_read_string_32
+ end
+
+ CMS_CORE_STORAGE_SQL_I
+ redefine
+ sql_read_date_time, sql_read_integer_32,
+ sql_read_string_32
+ end
+
+ CMS_USER_STORAGE_SQL_I
+ redefine
+ sql_read_date_time, sql_read_integer_32,
+ sql_read_string_32
+ end
+
+ SQLITE_BIND_ARG_MARSHALLER
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (db: SQLITE_DATABASE)
+ do
+ sqlite := db
+ create error_handler.make
+ end
+
+ sqlite: SQLITE_DATABASE
+ -- Associated SQLite database.
+
+feature -- Status report
+
+ is_initialized: BOOLEAN
+ -- Is storage initialized?
+ do
+ Result := has_user
+ end
+
+feature -- Status report
+
+ is_available: BOOLEAN
+ -- Is storage available?
+ do
+ Result := sqlite.is_interface_usable
+ end
+
+feature -- Basic operation
+
+ close
+ -- Close/disconnect current storage.
+ do
+ sqlite.close
+ end
+
+feature -- Execution
+
+ transaction_depth: INTEGER
+
+ sql_begin_transaction
+ -- Start a database transtaction.
+ do
+ if transaction_depth = 0 then
+ sqlite.begin_transaction (False)
+ end
+ transaction_depth := transaction_depth + 1
+ debug ("roc_storage")
+ print ("# sql_begin_transaction (depth="+ transaction_depth.out +").%N")
+ end
+ end
+
+ sql_rollback_transaction
+ -- Rollback updates in the database.
+ do
+ if sqlite.is_in_transaction then
+ sqlite.rollback
+ end
+ transaction_depth := transaction_depth - 1
+ debug ("roc_storage")
+ print ("# sql_rollback_transaction (depth="+ transaction_depth.out +").%N")
+ end
+ end
+
+ sql_commit_transaction
+ -- Commit updates in the database.
+ do
+ if sqlite.is_in_transaction then
+ sqlite.commit
+ end
+ transaction_depth := transaction_depth - 1
+ debug ("roc_storage")
+ print ("# sql_commit_transaction (depth="+ transaction_depth.out +").%N")
+ end
+ end
+
+ sql_post_execution
+ -- Post database execution.
+ -- note: execute after each `sql_query' and `sql_change'.
+ do
+ debug ("roc_storage")
+ print ("# sql_post_execution.%N")
+ end
+ -- FIXME
+ if sqlite.has_error then
+ write_critical_log (generator + ".post_execution Error occurred!")
+ end
+ end
+
+feature -- Operation
+
+ last_statement: detachable SQLITE_STATEMENT
+
+ last_sqlite_result_cursor: detachable SQLITE_STATEMENT_ITERATION_CURSOR
+
+ sql_query (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ --
+ local
+ st: SQLITE_QUERY_STATEMENT
+ do
+ debug ("roc_storage")
+ print ("> sql_query (" +a_sql_statement + ").%N")
+ end
+ last_sqlite_result_cursor := Void
+ create st.make (a_sql_statement, sqlite)
+ last_statement := st
+ if st.is_compiled then
+ if a_params /= Void then
+ check st.has_arguments end
+ last_sqlite_result_cursor := st.execute_new_with_arguments (sqlite_arguments (a_params))
+ else
+ last_sqlite_result_cursor := st.execute_new
+ end
+ else
+ error_handler.add_custom_error (1, "invalid query", "query compilation failed!")
+ end
+ debug ("roc_storage")
+ print ("< sql_query (" +a_sql_statement + ").%N")
+ end
+ end
+
+ sql_finalize
+ -- Finalize sql query (i.e destroy previous query statement.
+ do
+ debug ("roc_storage")
+ print ("> sql_finalize.%N")
+ end
+ if attached last_statement as st then
+ st.cleanup
+ end
+ if attached last_sqlite_result_cursor as cur then
+ if cur.statement /= last_statement then
+ check should_not_occurs: False end
+ cur.statement.cleanup
+ end
+ last_sqlite_result_cursor := Void
+ end
+ last_statement := Void
+ debug ("roc_storage")
+ print ("< sql_finalize.%N")
+ end
+ end
+
+ sql_insert (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ --
+ local
+ st: SQLITE_INSERT_STATEMENT
+ do
+ debug ("roc_storage")
+ print ("> sql_insert (" +a_sql_statement + ").%N")
+ end
+ last_sqlite_result_cursor := Void
+ create st.make (a_sql_statement, sqlite)
+ last_statement := st
+ if st.is_compiled then
+ if a_params /= Void then
+ check st.has_arguments end
+ last_sqlite_result_cursor := st.execute_new_with_arguments (sqlite_arguments (a_params))
+ else
+ last_sqlite_result_cursor := st.execute_new
+ end
+ else
+ error_handler.add_custom_error (1, "invalid query", "query compilation failed!")
+ end
+ debug ("roc_storage")
+ print ("< sql_insert (" +a_sql_statement + ").%N")
+ end
+ end
+
+ sql_modify (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY])
+ --
+ local
+ st: SQLITE_MODIFY_STATEMENT
+ do
+ debug ("roc_storage")
+ print ("> sql_modify (" +a_sql_statement + ").%N")
+ end
+ last_sqlite_result_cursor := Void
+ create st.make (a_sql_statement, sqlite)
+ last_statement := st
+ if st.is_compiled then
+ if a_params /= Void then
+ check st.has_arguments end
+ last_sqlite_result_cursor := st.execute_new_with_arguments (sqlite_arguments (a_params))
+ else
+ last_sqlite_result_cursor := st.execute_new
+ end
+ else
+ error_handler.add_custom_error (1, "invalid query", "query compilation failed!")
+ end
+ debug ("roc_storage")
+ print ("< sql_modify (" +a_sql_statement + ").%N")
+ end
+ end
+
+ sqlite_arguments (a_params: STRING_TABLE [detachable ANY]): ARRAYED_LIST [SQLITE_BIND_ARG [ANY]]
+ local
+ k: READABLE_STRING_GENERAL
+ k8: STRING
+ utf: UTF_CONVERTER
+ do
+ create Result.make (a_params.count)
+ across
+ a_params as ic
+ loop
+ k := ic.key
+ if k.is_valid_as_string_8 then
+ k8 := k.as_string_8
+ else
+ k8 := utf.utf_32_string_to_utf_8_string_8 (k)
+ end
+ if attached {DATE_TIME} ic.item as dt then
+ Result.force (new_binding_argument (date_time_to_string (dt), ":" + k8))
+ elseif attached {READABLE_STRING_32} ic.item as s32 then
+ Result.force (new_binding_argument (utf.utf_32_string_to_utf_8_string_8 (s32), ":" + k8))
+ else
+ Result.force (new_binding_argument (ic.item, ":" + k8))
+ end
+ end
+ end
+
+ date_time_to_string (dt: DATE_TIME): STRING
+ do
+ create Result.make (16)
+ Result.append_integer (dt.year)
+ Result.append_character ('-')
+ if dt.month <= 9 then
+ Result.append_character ('0')
+ end
+ Result.append_integer (dt.month)
+ Result.append_character ('-')
+ if dt.day <= 9 then
+ Result.append_character ('0')
+ end
+ Result.append_integer (dt.day)
+ Result.append_character (' ')
+ if dt.hour <= 9 then
+ Result.append_character ('0')
+ end
+ Result.append_integer (dt.hour)
+ Result.append_character (':')
+ if dt.minute <= 9 then
+ Result.append_character ('0')
+ end
+ Result.append_integer (dt.minute)
+ Result.append_character (':')
+ if dt.second <= 9 then
+ Result.append_character ('0')
+ end
+ Result.append_integer (dt.second)
+ end
+
+ string_to_date_time (a_string: READABLE_STRING_GENERAL): DATE_TIME
+ local
+ y,m,d: INTEGER
+ h,min,sec: INTEGER
+ s: detachable READABLE_STRING_GENERAL
+ i,j: INTEGER
+ do
+ i := 1
+ -- YYYY
+ j := a_string.index_of ('-', i)
+ s := a_string.substring (i, j - 1)
+ y := s.to_integer
+ i := j + 1
+ -- /MM
+ j := a_string.index_of ('-', i)
+ s := a_string.substring (i, j - 1)
+ m := s.to_integer
+ i := j + 1
+ -- /DD
+ j := a_string.index_of (' ', i)
+ s := a_string.substring (i, j - 1)
+ d := s.to_integer
+ i := j + 1
+ -- %THour
+ j := a_string.index_of (':', i)
+ s := a_string.substring (i, j - 1)
+ h := s.to_integer
+ i := j + 1
+ -- :Min
+ j := a_string.index_of (':', i)
+ s := a_string.substring (i, j - 1)
+ min := s.to_integer
+ i := j + 1
+ -- :Sec
+ j := a_string.count + 1
+ s := a_string.substring (i, j - 1)
+ sec := s.to_integer
+
+ create Result.make (y,m,d,h,min,sec)
+ end
+
+feature -- Access
+
+ sql_start
+ -- .
+ do
+ -- sqlite cursor `last_sqlite_result_cursor', already at first position if any.
+ end
+
+ sql_after: BOOLEAN
+ -- .
+ do
+ if attached last_sqlite_result_cursor as l_cursor then
+ Result := l_cursor.after
+ end
+ end
+
+ sql_forth
+ -- .
+ do
+ if attached last_sqlite_result_cursor as l_cursor then
+ l_cursor.forth
+ end
+ end
+
+ sql_valid_item_index (a_index: INTEGER): BOOLEAN
+ local
+ l_row: SQLITE_RESULT_ROW
+ do
+ if attached last_sqlite_result_cursor as l_cursor then
+ l_row := l_cursor.item
+ Result := a_index > 0 and a_index.to_natural_32 <= l_row.count
+ end
+ end
+
+ sql_item (a_index: INTEGER): detachable ANY
+ local
+ l_row: SQLITE_RESULT_ROW
+ do
+ if attached last_sqlite_result_cursor as l_cursor then
+ l_row := l_cursor.item
+ Result := l_row.value (a_index.to_natural_32)
+ end
+ end
+
+ sql_read_string_32 (a_index: INTEGER): detachable STRING_32
+ --
+ local
+ utf: UTF_CONVERTER
+ do
+ Result := Precursor (a_index)
+ if Result = Void then
+ if attached sql_read_string (a_index) as s8 then
+ Result := utf.utf_8_string_8_to_string_32 (s8)
+ end
+ end
+ end
+
+ sql_read_integer_32 (a_index: INTEGER): INTEGER_32
+ -- Retrieved value at `a_index' position in `item'.
+ local
+ l_item: like sql_item
+ i64: INTEGER_64
+ do
+ l_item := sql_item (a_index)
+ if attached {INTEGER_32} l_item as i then
+ Result := i
+ elseif attached {INTEGER_32_REF} l_item as l_value then
+ Result := l_value.item
+ else
+ if attached {INTEGER_64} l_item as i then
+ i64 := i
+ elseif attached {INTEGER_64_REF} l_item as l_value then
+ i64 := l_value.item
+ else
+ check is_integer_32: False end
+ end
+ if i64 <= {INTEGER_32}.max_value then
+ Result := i64.to_integer_32
+ else
+ check is_integer_32: False end
+ end
+ end
+ end
+
+ sql_read_date_time (a_index: INTEGER): detachable DATE_TIME
+ -- Retrieved value at `a_index' position in `item'.
+ local
+ l_item: like sql_item
+ do
+ l_item := sql_item (a_index)
+ if attached {DATE_TIME} l_item as dt then
+ Result := dt
+ elseif attached {READABLE_STRING_GENERAL} l_item as s then
+ Result := string_to_date_time (s)
+ else
+ check is_date_time_nor_null: l_item = Void end
+ end
+ end
+
+feature -- Conversion
+
+ sql_statement (a_statement: STRING): STRING
+ -- .
+ local
+ i: INTEGER
+ do
+ Result := a_statement
+ from
+ i := 1
+ until
+ i = 0
+ loop
+ i := a_statement.substring_index ("AUTO_INCREMENT", i)
+ if i > 0 then
+ if Result = a_statement then
+ create Result.make_from_string (a_statement)
+ end
+ Result.remove (i + 4)
+ i := i + 14
+ end
+ end
+ end
+
+end
diff --git a/library/persistence/sqlite3/src/cms_storage_sqlite3_builder.e b/library/persistence/sqlite3/src/cms_storage_sqlite3_builder.e
new file mode 100644
index 0000000..ce5c267
--- /dev/null
+++ b/library/persistence/sqlite3/src/cms_storage_sqlite3_builder.e
@@ -0,0 +1,90 @@
+note
+ description: "[
+ Objects that ...
+ ]"
+ author: "$Author: jfiat $"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_STORAGE_SQLITE3_BUILDER
+
+inherit
+ CMS_STORAGE_SQL_BUILDER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Initialize `Current'.
+ do
+ end
+
+feature -- Factory
+
+ storage (a_setup: CMS_SETUP; a_error_handler: ERROR_HANDLER): detachable CMS_STORAGE_SQLITE3
+ local
+ s: detachable READABLE_STRING_32
+ p: PATH
+ db: detachable SQLITE_DATABASE
+ l_source: SQLITE_FILE_SOURCE
+ i,j: INTEGER
+ do
+ if
+ attached (create {APPLICATION_JSON_CONFIGURATION_HELPER}).new_database_configuration (a_setup.environment.application_config_path) as l_database_config
+ then
+ if l_database_config.driver.is_case_insensitive_equal ("sqlite3") then
+ s := l_database_config.database_string
+ i := s.substring_index ("Database=", 1)
+ if i > 0 then
+ i := s.index_of ('=', i) + 1
+ j := s.index_of (';', i)
+ if j = 0 then
+ j := s.count + 1
+ end
+ create p.make_from_string (s.substring (i, j - 1))
+ else
+ create p.make_from_string (s)
+ end
+
+ if attached reuseable_connection.item as d then
+ if p.same_as (d.path) then
+ db := d.database
+ end
+ end
+ if db = Void or else db.is_closed then
+ create l_source.make (p.name)
+ create db.make (l_source)
+ if l_source.exists then
+ db.open_read_write
+ else
+ db.open_create_read_write
+ end
+ end
+ if not db.is_closed then
+ db.set_busy_timeout (1_000) -- FIXME
+ create Result.make (db)
+-- set_map_zero_null_value (False) --| This way we map 0 to 0, instead of Null as default.
+ if Result.is_available then
+ if not Result.is_initialized then
+ initialize (a_setup, Result)
+ end
+ end
+ else
+ a_error_handler.add_custom_error (0, "Could not connect to the ODBC storage", Void)
+ end
+ else
+ -- Wrong mapping between storage name and storage builder!
+ end
+ end
+ end
+
+ reuseable_connection: CELL [detachable TUPLE [path: PATH; database: SQLITE_DATABASE]]
+ once
+ create Result.put (Void)
+ end
+
+
+end
diff --git a/library/persistence/store_mysql/src/cms_storage_store_mysql.e b/library/persistence/store_mysql/src/cms_storage_store_mysql.e
new file mode 100644
index 0000000..80f5586
--- /dev/null
+++ b/library/persistence/store_mysql/src/cms_storage_store_mysql.e
@@ -0,0 +1,37 @@
+note
+ description: "Summary description for {CMS_STORAGE_STORE_MYSQL}."
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_STORAGE_STORE_MYSQL
+
+inherit
+ CMS_STORAGE_STORE_SQL
+
+ CMS_CORE_STORAGE_SQL_I
+
+ CMS_USER_STORAGE_SQL_I
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Status report
+
+ is_initialized: BOOLEAN
+ -- Is storage initialized?
+ do
+ Result := has_user
+ end
+
+feature -- Conversion
+
+ sql_statement (a_statement: STRING): STRING
+ --
+ do
+ Result := a_statement
+ end
+
+end
diff --git a/library/persistence/store_mysql/src/cms_storage_store_mysql_builder.e b/library/persistence/store_mysql/src/cms_storage_store_mysql_builder.e
new file mode 100644
index 0000000..4ea17aa
--- /dev/null
+++ b/library/persistence/store_mysql/src/cms_storage_store_mysql_builder.e
@@ -0,0 +1,51 @@
+note
+ description: "[
+ Interface responsible to instantiate CMS_STORAGE_STORE_MYSQL object.
+ ]"
+ author: "$Author: jfiat $"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ CMS_STORAGE_STORE_MYSQL_BUILDER
+
+inherit
+ CMS_STORAGE_STORE_SQL_BUILDER
+
+ GLOBAL_SETTINGS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Initialize `Current'.
+ do
+ end
+
+feature -- Factory
+
+ storage (a_setup: CMS_SETUP; a_error_handler: ERROR_HANDLER): detachable CMS_STORAGE_STORE_MYSQL
+ local
+ conn: DATABASE_CONNECTION
+ do
+ if attached (create {APPLICATION_JSON_CONFIGURATION_HELPER}).new_database_configuration (a_setup.environment.application_config_path) as l_database_config then
+ create {DATABASE_CONNECTION_MYSQL} conn.login_with_connection_string (l_database_config.connection_string)
+ if conn.is_connected then
+ create Result.make (conn)
+ set_map_zero_null_value (False) --| This way we map 0 to 0, instead of Null as default.
+ set_use_extended_types (True) --| Use extended types: STRING_32 etc.
+ if Result.is_available then
+ if not Result.is_initialized then
+ initialize (a_setup, Result)
+ end
+ end
+ else
+ a_error_handler.add_custom_error (0, "Could not connect to the MySQL storage", Void)
+ end
+ end
+ end
+
+
+end
diff --git a/library/persistence/store_mysql/src/database/database_connection_mysql.e b/library/persistence/store_mysql/src/database/database_connection_mysql.e
new file mode 100644
index 0000000..1a95b6a
--- /dev/null
+++ b/library/persistence/store_mysql/src/database/database_connection_mysql.e
@@ -0,0 +1,115 @@
+note
+ description: "Object that handle a database connection for ODBC"
+ date: "$Date: 2014-08-20 15:21:15 -0300 (mi., 20 ago. 2014) $"
+ revision: "$Revision: 95678 $"
+
+class
+ DATABASE_CONNECTION_MYSQL
+
+inherit
+
+ DATABASE_CONNECTION
+ redefine
+ db_application
+ end
+
+create
+ login, login_with_default, login_with_database_name, login_with_connection_string, login_with_schema
+
+feature -- Initialization
+
+ login_with_default
+ -- Create a database handler for MYSQL with common settings.
+ do
+ login_with_database_name (default_database_name)
+ end
+
+ login_with_database_name (a_database_name: STRING)
+ -- Create a database handler and
+ -- set database_name to `a_database_name'.
+ do
+ login (default_username, default_password, default_hostname, a_database_name, is_keep_connection)
+ end
+
+ login (a_username: STRING; a_password: STRING; a_hostname: STRING; a_database_name: STRING; connection: BOOLEAN)
+
+ -- Create a database handler for ODBC and set `username' to `a_username',
+ -- `password' to `a_password'
+ -- `database_name' to `a_database_name'
+ -- `connection' to `a_connection'
+ do
+ create database_error_handler.make
+ create db_application.login (a_username, a_password)
+ db_application.set_hostname (a_hostname)
+ db_application.set_data_source (a_database_name)
+ db_application.set_base
+ create db_control.make
+ keep_connection := connection
+ if keep_connection then
+ connect
+ end
+ end
+
+ login_with_connection_string (a_string: STRING)
+ -- Login with `a_connection_string' and immediately connect to database.
+ local
+ l_server: STRING
+ l_port: STRING
+ l_database: STRING
+ l_user: STRING
+ l_password: STRING
+ do
+ create database_error_handler.make
+ l_server := connection_string_item (a_string, "Server", default_hostname)
+ l_database := connection_string_item (a_string, "Database", default_database_name)
+ l_port := connection_string_item (a_string, "Port", "3306")
+ l_user := connection_string_item (a_string, "Uid", default_username)
+ l_password := connection_string_item (a_string, "Pwd", default_password)
+
+ create db_application
+ db_application.set_application (l_database)
+ db_application.set_hostname (l_server + ":" + l_port)
+ db_application.login_and_connect (l_user, l_password)
+ db_application.set_base
+ create db_control.make
+ keep_connection := is_keep_connection
+ end
+
+ connection_string_item (a_connection_string: STRING; k: STRING; dft: STRING): STRING
+ local
+ i,j: INTEGER
+ do
+ i := a_connection_string.substring_index (k + "=", 1)
+ if i = 0 then
+ i := a_connection_string.substring_index (k.as_lower + "=", 1)
+ end
+ if i > 0 then
+ i := i + k.count + 1
+ j := a_connection_string.index_of (';', i)
+ if j = 0 then
+ j := a_connection_string.count + 1
+ end
+ Result := a_connection_string.substring (i, j - 1)
+ else
+ Result := dft
+ end
+ end
+
+ login_with_schema (a_schema: STRING; a_username: STRING; a_password: STRING)
+ -- Login with `a_connection_string'and immediately connect to database.
+ do
+ create database_error_handler.make
+ create db_application
+ db_application.set_application (a_schema)
+ db_application.login_and_connect (a_username, a_password)
+ db_application.set_base
+ create db_control.make
+ keep_connection := is_keep_connection
+ end
+
+feature -- Databse Connection
+
+ db_application: DATABASE_APPL [MYSQL]
+ -- Database application.
+
+end
diff --git a/library/persistence/store_mysql/store_mysql-safe.ecf b/library/persistence/store_mysql/store_mysql-safe.ecf
new file mode 100644
index 0000000..b397e9c
--- /dev/null
+++ b/library/persistence/store_mysql/store_mysql-safe.ecf
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /database/database_connection_odbc.e
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/persistence/store_mysql/tests/application.e b/library/persistence/store_mysql/tests/application.e
new file mode 100644
index 0000000..63f6c03
--- /dev/null
+++ b/library/persistence/store_mysql/tests/application.e
@@ -0,0 +1,73 @@
+note
+ description : "tests application root class"
+ date : "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision : "$Revision: 96542 $"
+
+class
+ APPLICATION
+
+inherit
+ ARGUMENTS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Run application.
+ local
+ storage: CMS_STORAGE
+ l_node: CMS_NODE
+ do
+ create connection.login_with_schema ("cms_dev", "root", "")
+
+ (create {CLEAN_DB}).clean_db(connection)
+
+ create {CMS_STORAGE_STORE_MYSQL} storage.make (connection)
+ l_node := custom_node ("Content", "Summary", "Title")
+ storage.new_user (default_user)
+ storage.new_user (custom_user ("u2", "p2", "e2"))
+ l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_title (2,ll_node.id, "New Title")
+-- check
+-- attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then u_node.content ~ ll_node.content and then u_node.summary ~ ll_node.summary
+-- end
+-- end
+ end
+
+
+feature {NONE} -- Fixture Factory: Users
+
+ default_user: CMS_USER
+ do
+ Result := custom_user ("test", "password", "test@test.com")
+ end
+
+ custom_user (a_name, a_password, a_email: READABLE_STRING_32): CMS_USER
+ do
+ create Result.make (a_name)
+ Result.set_password (a_password)
+ Result.set_email (a_email)
+ end
+
+feature {NONE} -- Implementation
+
+
+ connection: DATABASE_CONNECTION_MYSQL
+
+
+ default_node: CMS_NODE
+ do
+ Result := custom_node ("Default content", "default summary", "Default")
+ end
+
+ custom_node (a_content, a_summary, a_title: READABLE_STRING_32): CMS_PAGE
+ do
+ create Result.make (a_title)
+ Result.set_content (a_content, a_summary, Void)
+ end
+
+end
diff --git a/library/persistence/store_mysql/tests/handler/database_handler_test.e b/library/persistence/store_mysql/tests/handler/database_handler_test.e
new file mode 100644
index 0000000..6ae64b4
--- /dev/null
+++ b/library/persistence/store_mysql/tests/handler/database_handler_test.e
@@ -0,0 +1,81 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date$"
+ revision: "$Revision$"
+ testing: "type/manual"
+
+class
+ DATABASE_HANDLER_TEST
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+
+feature -- Test routines
+
+ test_wrong_database_query
+ -- New test routine
+ local
+ l_parameters: STRING_TABLE[detachable ANY]
+ do
+ create l_parameters.make (0)
+ handler.set_query (create {DATABASE_QUERY}.data_reader ("Sellect from users", l_parameters))
+ handler.execute_query
+ assert ("Has error:", handler.has_error)
+ end
+
+
+
+ test_sequences_of_wrong_and_correct_queries
+ -- New test routine
+ local
+ l_parameters: STRING_TABLE[detachable ANY]
+ do
+ create l_parameters.make (0)
+ handler.set_query (create {DATABASE_QUERY}.data_reader ("Sellect from users;", l_parameters))
+ handler.execute_query
+ assert ("Has error:", handler.has_error)
+
+ handler.set_query (create {DATABASE_QUERY}.data_reader ("Select * from users;", l_parameters))
+ handler.execute_query
+ assert ("Not Has error:",not handler.has_error)
+ end
+
+
+feature -- Handler
+
+ handler: DATABASE_HANDLER
+ once
+ create {DATABASE_HANDLER_IMPL} Result.make (connection )
+ end
+
+end
+
+
diff --git a/library/persistence/store_mysql/tests/nodes/node_test_set.e b/library/persistence/store_mysql/tests/nodes/node_test_set.e
new file mode 100644
index 0000000..bd85896
--- /dev/null
+++ b/library/persistence/store_mysql/tests/nodes/node_test_set.e
@@ -0,0 +1,242 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ testing:"execution/isolated"
+
+class
+ NODE_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db (connection)
+ assert ("Empty Nodes", storage.nodes_count = 0)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_new_node
+ note
+ testing: "execution/isolated"
+ do
+ assert ("Empty Nodes", storage.nodes_count = 0)
+ storage.new_node (default_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached storage.node_by_id (1))
+ -- Not exist node with id 2
+ assert ("Not exist node with id 2", storage.node_by_id (2) = Void)
+ end
+
+
+ test_update_node
+ note
+ testing: "execution/isolated"
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node (content and summary)
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ l_un.set_content ("Updating test node udpate ")
+ l_un.set_summary ("updating summary")
+ storage.update_node (l_un)
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_node.content) and then not (ll_node.summary ~ l_node.summary) and then ll_node.title ~ l_node.title )
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title )
+ end
+
+ -- Update node (content and summary and title)
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ l_un.set_content ("Updating test node udpate ")
+ l_un.set_summary ("updating summary")
+ l_un.set_title ("Updating Test case")
+ storage.update_node (l_un)
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_node.content) and then not (ll_node.summary ~ l_node.summary) and then not (ll_node.title ~ l_node.title) )
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title )
+ end
+ end
+
+ test_update_title
+ note
+ testing: "execution/isolated"
+ local
+ l_node: CMS_NODE
+ u: CMS_USER
+ do
+ u := default_user
+ storage.new_user (u)
+
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node title
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_title (u.id, l_un.id, "New Title")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then not ( ll_node.title ~ l_un.title) and then ll_node.title ~ "New Title" )
+ end
+ end
+
+ test_update_summary
+ note
+ testing: "execution/isolated"
+ local
+ l_node: CMS_NODE
+ u: CMS_USER
+ do
+ u := default_user
+ storage.new_user (u)
+
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node summary
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_summary (u.id, l_un.id, "New Summary")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then not (ll_node.summary ~ l_un.summary) and then ll_node.summary ~ "New Summary" and then ll_node.title ~ l_un.title)
+ end
+ end
+
+ test_update_content
+ note
+ testing: "execution/isolated"
+ local
+ l_node: CMS_NODE
+ u: CMS_USER
+ do
+ u := default_user
+ storage.new_user (u)
+
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.sql_begin_transaction
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node content
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_content (u.id, l_un.id, "New Content")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_un.content) and then ll_node.content ~ "New Content" and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title)
+ end
+ storage.sql_commit_transaction
+ end
+
+
+ test_delete_node
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Delte node 1
+
+ storage.delete_node_by_id (1)
+ assert ("Node does not exist", storage.node_by_id (1) = Void)
+ end
+
+ test_recent_nodes
+ -- Content_10, Summary_10, Title_10
+ -- Content_9, Summary_9, Title_9
+ -- ..
+ -- Content_1, Summary_1, Title_1
+ local
+ i : INTEGER
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ across 1 |..| 10 as c loop
+ storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+ end
+
+ -- Scenario (0,10) rows, recents (10 down to 1)
+ i := 10
+ across storage.recent_nodes (0, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenario (5, 10) rows, recent nodes (5 down to 1)
+ i := 5
+ across storage.recent_nodes (5, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenario (9,10) rows, recent node 1
+ i := 1
+ across storage.recent_nodes (9, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenrario 10..10 empty
+ assert ("Empty", storage.recent_nodes (10, 10).after)
+ end
+
+ test_new_node_add_author
+ do
+ storage.new_node (default_node)
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1))
+ storage.new_user (custom_user ("u1", "u1", "email"))
+ if attached storage.user_by_name ("u1") as l_user then
+ if attached storage.node_by_id (1) as l_node then
+ l_node.set_author (l_user)
+ storage.update_node (l_node)
+ assert ("Author not void for node 1", attached storage.node_author (1))
+ end
+ end
+ end
+
+
+end
+
+
+
diff --git a/library/persistence/store_mysql/tests/storage/storage_test_set.e b/library/persistence/store_mysql/tests/storage/storage_test_set.e
new file mode 100644
index 0000000..e91d208
--- /dev/null
+++ b/library/persistence/store_mysql/tests/storage/storage_test_set.e
@@ -0,0 +1,358 @@
+note
+ description: "Summary description for {STORAGE_TEST_SET}."
+ author: ""
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ STORAGE_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_has_user
+ do
+ assert ("Not has user", not storage.has_user)
+ end
+
+ test_all_users
+ do
+ assert ("to implement all_users", False)
+ end
+
+ test_user_by_id_not_exist
+ do
+ assert ("User does not exist", storage.user_by_id (1) = Void)
+ end
+
+ test_user_by_name_not_exist
+ do
+ assert ("User does not exist", storage.user_by_name ("test") = Void)
+ end
+
+ test_user_by_email_not_exist
+ do
+ assert ("User does not exist", storage.user_by_name ("test@test.com") = Void)
+ end
+
+ test_user_with_bad_id
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_id (0)
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_user_with_bad_name_empty
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_name ("")
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_user_with_bad_email_empty
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_email ("")
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_new_user
+ do
+ storage.new_user (default_user)
+ assert ("Has user", storage.has_user)
+ end
+
+ test_user_by_id
+ do
+ storage.new_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_id (1) as l_user then
+ assert ("Exist", True)
+ assert ("User test", l_user.name ~ "test")
+ assert ("User id = 1", l_user.id = 1)
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_user_by_name
+ do
+ storage.new_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_name ("test") as l_user then
+ assert ("Exist", True)
+ assert ("User nane: test", l_user.name ~ "test")
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_user_by_email
+ do
+ storage.new_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_email ("test@test.com") as l_user then
+ assert ("Exist", True)
+ assert ("User email: test@test.com", l_user.email ~ "test@test.com")
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_invalid_credential
+ do
+ storage.new_user (default_user)
+ assert ("Has user test", attached storage.user_by_name ("test"))
+ assert ("Wrong password", not storage.is_valid_credential ("test", "test"))
+ assert ("Wrong user", not storage.is_valid_credential ("test1", "test"))
+ end
+
+ test_valid_credential
+ do
+ storage.new_user (default_user)
+ assert ("Has user test", attached storage.user_by_name ("test"))
+ assert ("Valid password", storage.is_valid_credential ("test", "password"))
+ end
+
+-- test_recent_nodes_empty
+-- do
+-- assert ("No recent nodes", storage.recent_nodes (0, 10).is_empty)
+-- end
+
+-- test_recent_nodes
+-- local
+-- l_nodes: LIST[CMS_NODE]
+-- l_node: CMS_NODE
+-- do
+-- storage.new_user (default_user)
+-- across 1 |..| 10 as c loop
+-- l_node := custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- end
+-- l_nodes := storage.recent_nodes (0, 10)
+-- assert ("10 recent nodes", l_nodes.count = 10)
+-- assert ("First node id=10", l_nodes.first.id = 10)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+
+-- l_nodes := storage.recent_nodes (5, 10)
+-- assert ("5 recent nodes", l_nodes.count = 5)
+-- assert ("First node id=5", l_nodes.first.id = 5)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+-- l_nodes := storage.recent_nodes (9, 10)
+-- assert ("1 recent nodes", l_nodes.count = 1)
+-- assert ("First node id=1", l_nodes.first.id = 1)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+-- l_nodes := storage.recent_nodes (10, 10)
+-- assert ("Is empty", l_nodes.is_empty)
+-- end
+
+-- test_node_does_not_exist
+-- local
+-- l_node: CMS_NODE
+-- do
+-- storage.new_user (default_user)
+-- across 1 |..| 10 as c loop
+-- l_node := custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- end
+-- assert ("Not exist node id: 12", storage.node_by_id (12) = Void)
+-- end
+
+-- test_node
+-- local
+-- l_node: CMS_NODE
+-- do
+-- storage.new_user (default_user)
+-- across 1 |..| 10 as c loop
+-- l_node := custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- end
+-- assert ("Node id: 10", attached storage.node_by_id (10) as ll_node and then ll_node.title ~ "Title_10" )
+-- end
+
+-- test_update_node
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- l_node := ll_node.twin
+-- l_node.set_content ("New Content")
+-- l_node.set_summary ("New Summary")
+-- l_node.set_title("New Title")
+-- if attached storage.user_by_email (default_user.email) as l_user then
+-- l_node.set_author (l_user)
+-- storage.update_node (l_node)
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then not (u_node.content ~ ll_node.content) and then not (u_node.summary ~ ll_node.summary))
+-- end
+-- end
+-- end
+
+-- test_update_node_title
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- storage.new_user (custom_user ("u2", "p2", "e2"))
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_title (2,ll_node.id, "New Title")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then u_node.content ~ ll_node.content and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+-- test_update_node_summary
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- storage.new_user (custom_user ("u2", "p2", "e2"))
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_summary (2,ll_node.id, "New Summary")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then u_node.content ~ ll_node.content and then not (u_node.summary ~ ll_node.summary))
+-- end
+-- end
+
+-- test_update_node_content
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- storage.new_user (custom_user ("u2", "p2", "e2"))
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_content (2,ll_node.id, "New Content")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then not (u_node.content ~ ll_node.content) and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+
+-- test_update_node_title_by_author
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_title (1,ll_node.id, "New Title")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then u_node.content ~ ll_node.content and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+-- test_update_node_summary_by_author
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_summary (1,ll_node.id, "New Summary")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then u_node.content ~ ll_node.content and then not (u_node.summary ~ ll_node.summary))
+-- end
+-- end
+
+-- test_update_node_content_by_author
+-- local
+-- l_node: CMS_NODE
+-- do
+-- l_node := custom_node ("Content", "Summary", "Title")
+-- storage.new_user (default_user)
+-- l_node.set_author (storage.user_by_email (default_user.email))
+-- storage.new_node (l_node)
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- storage.update_node_content (1,ll_node.id, "New Content")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then not (u_node.content ~ ll_node.content) and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+-- test_delete_node
+-- local
+-- l_node: CMS_NODE
+-- l_user: like {CMS_NODE}.author
+-- do
+-- storage.new_user (custom_user ("test_delete", "testu", "email"))
+-- l_user := storage.user_by_name ("test_delete")
+-- across 1 |..| 10 as c loop
+-- l_node := custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out)
+-- l_node.set_author (l_user)
+-- storage.new_node (l_node)
+-- end
+-- assert ("Exist node id: 10", attached storage.node_by_id (10) as ll_node and then ll_node.title ~ "Title_10" )
+-- storage.delete_node_by_id (10)
+-- assert ("Not exist node id: 10", storage.node_by_id (10) = Void)
+-- end
+
+end
diff --git a/library/persistence/store_mysql/tests/tests.ecf b/library/persistence/store_mysql/tests/tests.ecf
new file mode 100644
index 0000000..7ce81ca
--- /dev/null
+++ b/library/persistence/store_mysql/tests/tests.ecf
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+ /nodes$
+
+
+
+
diff --git a/library/persistence/store_mysql/tests/transactions/transaction_test_set.e b/library/persistence/store_mysql/tests/transactions/transaction_test_set.e
new file mode 100644
index 0000000..e3ed437
--- /dev/null
+++ b/library/persistence/store_mysql/tests/transactions/transaction_test_set.e
@@ -0,0 +1,87 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ testing: "type/manual"
+
+class
+ TRANSACTION_TEST_SET
+
+inherit
+
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db (connection)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_user_rollback
+ note
+ testing: "execution/isolated"
+ local
+ u: detachable CMS_USER
+ do
+ u := storage.user_by_name ("test")
+ if u = Void then
+ u := custom_user ("test", "test","test@admin.com")
+ storage.new_user (u)
+ end
+ assert ("Has user:", storage.has_user)
+ u.set_email ("test@example.com")
+ storage.sql_begin_transaction
+ storage.update_user (u)
+ assert ("Has user:", storage.user_by_email ("test@example.com") /= Void)
+ storage.sql_rollback_transaction
+ assert ("Not has user:", storage.user_by_email ("test@example.com") = Void)
+ end
+
+ test_user_node_rollback
+ note
+ testing: "execution/isolated"
+ local
+ u: detachable CMS_USER
+ do
+ u := storage.user_by_name ("test")
+ if u = Void then
+ u := custom_user ("test", "test","test@admin.com")
+ storage.new_user (u)
+ end
+
+ connection.begin_transaction
+ u.set_email ("test@example.com")
+ assert ("Has user:", storage.user_by_email ("test@example.com") /= Void)
+-- storage.new_node (default_node)
+-- assert ("Has one node:", storage.nodes_count = 1)
+ connection.rollback
+ assert ("Not has user:", storage.user_by_email ("test@example.com") = Void)
+-- assert ("Has no node:", storage.nodes_count = 0)
+ end
+
+end
+
+
diff --git a/library/persistence/store_mysql/tests/users/user_test_set.e b/library/persistence/store_mysql/tests/users/user_test_set.e
new file mode 100644
index 0000000..3ae13b3
--- /dev/null
+++ b/library/persistence/store_mysql/tests/users/user_test_set.e
@@ -0,0 +1,111 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ testing:"execution/isolated"
+
+class
+ USER_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ storage.new_user (custom_user ("admin", "admin","admin@admin.com"))
+ end
+
+ on_clean
+ --
+ do
+-- (create {CLEAN_DB}).clean_db(connection)
+ end
+
+feature -- Test routines
+
+ test_user_exist
+ -- User admin exist
+ do
+ assert ("Not void", attached storage.user_by_email ("admin@admin.com"))
+ assert ("Not void", attached storage.user_by_id (1))
+ assert ("Not void", attached storage.user_by_name ("admin"))
+ end
+
+ test_user_not_exist
+ -- Uset test does not exist.
+ do
+ assert ("User by email: Void", storage.user_by_email ("test1@test.com") = Void)
+ assert ("User by id: Void", storage.user_by_id (5) = Void )
+ assert ("User by name: Void", storage.user_by_name ("test1") = Void)
+ end
+
+ test_new_user
+ local
+ u: CMS_USER
+ do
+ u := default_user
+ storage.new_user (u)
+ assert ("Not void", attached storage.user_by_email (u.email))
+ assert ("Not void", attached storage.user_by_id (2))
+ assert ("Not void", attached storage.user_by_id (2) as l_user and then l_user.id = 2 and then l_user.name ~ u.name)
+ assert ("Not void", attached storage.user_by_name (u.name))
+ end
+
+-- test_new_user_with_roles
+-- do
+-- storage.new_user (default_user)
+-- storage.new_role ("Admin")
+-- assert ("Empty roles for given user", storage.user_roles (1).after)
+-- storage.add_role (1, 1)
+-- assert ("Not empty roles for given user", not storage.user_roles (1).after)
+-- end
+
+-- test_new_user_without_profile
+-- do
+-- storage.new_user ("test", "test","test@admin.com")
+-- assert ("Empty", storage.user_profile (1).new_cursor.after)
+-- end
+
+-- test_new_user_with_profile
+-- local
+-- l_profile: CMS_USER_PROFILE
+-- l_db_profile: CMS_USER_PROFILE
+-- do
+-- storage.new_user (default_user)
+-- if attached {CMS_USER} storage.user_by_name ("test") as l_user then
+-- assert ("Empty", storage.user_profile (l_user.id).new_cursor.after)
+-- create l_profile.make
+-- l_profile.force ("Eiffel", "language")
+-- l_profile.force ("Argentina", "country")
+-- l_profile.force ("GMT-3", "time zone")
+-- storage.save_profile (l_user.id, l_profile)
+-- l_db_profile := storage.user_profile (l_user.id)
+-- assert ("Not Empty", not l_db_profile.new_cursor.after)
+
+-- assert ("Expected language Eiffel", attached l_db_profile.item ("language") as l_language and then l_language ~ "Eiffel")
+-- assert ("Expected time zone GMT-3", attached l_db_profile.item ("time zone") as l_language and then l_language ~ "GMT-3")
+-- end
+-- end
+
+
+end
+
+
diff --git a/library/persistence/store_mysql/tests/util/abstract_db_test.e b/library/persistence/store_mysql/tests/util/abstract_db_test.e
new file mode 100644
index 0000000..7a1085c
--- /dev/null
+++ b/library/persistence/store_mysql/tests/util/abstract_db_test.e
@@ -0,0 +1,49 @@
+note
+ description: "Summary description for {ABSTRACT_DB_TEST}."
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ ABSTRACT_DB_TEST
+
+
+feature -- Database connection
+
+ connection: DATABASE_CONNECTION_MYSQL
+ -- MYSQL database connection
+ once
+ create Result.login_with_schema ("cms_dev", "root", "")
+ end
+
+ storage: CMS_STORAGE_STORE_MYSQL
+ once
+ create Result.make (connection)
+ end
+
+feature {NONE} -- Fixture Factory: Users
+
+ default_user: CMS_USER
+ do
+ Result := custom_user ("test", "password", "test@test.com")
+ end
+
+ custom_user (a_name, a_password: READABLE_STRING_32; a_email: READABLE_STRING_8): CMS_USER
+ do
+ create Result.make (a_name)
+ Result.set_password (a_password)
+ Result.set_email (a_email)
+ end
+
+feature {NONE} -- Fixture Factories: Nodes
+
+ default_node: CMS_NODE
+ do
+ Result := custom_node ("Default content", "default summary", "Default")
+ end
+
+ custom_node (a_content, a_summary, a_title: READABLE_STRING_32): CMS_PAGE
+ do
+ create Result.make (a_title)
+ Result.set_content (a_content, a_summary, Void)
+ end
+end
diff --git a/library/persistence/store_mysql/tests/util/clean_db.e b/library/persistence/store_mysql/tests/util/clean_db.e
new file mode 100644
index 0000000..dce7176
--- /dev/null
+++ b/library/persistence/store_mysql/tests/util/clean_db.e
@@ -0,0 +1,129 @@
+note
+ description: "[
+ Setting up database tests
+ 1. Put the database in a known state before running your test suite.
+ 2. Data reinitialization. For testing in developer sandboxes, something that you should do every time you rebuild the system, you may want to forgo dropping and rebuilding the database in favor of simply reinitializing the source data.
+ You can do this either by erasing all existing data and then inserting the initial data vales back into the database, or you can simple run updates to reset the data values.
+ The first approach is less risky and may even be faster for large amounts of data. - See more at: http://www.agiledata.org/essays/databaseTesting.html#sthash.6yVp35g8.dpuf
+ ]"
+
+ date: "$Date$"
+ revision: "$Revision$"
+ EIS: "name=Database Testing", "src=http://www.agiledata.org/essays/databaseTesting.html", "protocol=uri"
+ testing:"execution/serial"
+
+class
+ CLEAN_DB
+
+feature
+
+ clean_db (a_connection: DATABASE_CONNECTION)
+ -- Clean db test.
+ local
+ l_parameters: STRING_TABLE[STRING]
+ do
+ create l_parameters.make (0)
+
+ a_connection.begin_transaction
+
+ -- Clean Profiles
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_user_profiles, l_parameters))
+ db_handler(a_connection).execute_change
+
+ -- Clean Permissions
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_permissions, l_parameters))
+ db_handler(a_connection).execute_change
+
+
+ -- Clean Users Nodes
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_users_nodes, l_parameters))
+ db_handler(a_connection).execute_change
+
+
+ -- Clean Users Roles
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_users_roles, l_parameters))
+ db_handler(a_connection).execute_change
+
+ -- Clean Roles
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_roles, l_parameters))
+ db_handler(a_connection).execute_change
+
+ -- Clean Nodes
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_nodes, l_parameters))
+ db_handler(a_connection).execute_change
+
+ -- Clean Users
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_users, l_parameters))
+ db_handler(a_connection).execute_change
+
+
+ -- Reset Autoincremente
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_users_autoincrement, l_parameters))
+ db_handler(a_connection).execute_change
+
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_nodes_autoincrement, l_parameters))
+ db_handler(a_connection).execute_change
+
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_roles_autoincrement, l_parameters))
+ db_handler(a_connection).execute_change
+
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_permissions_autoincrement, l_parameters))
+ db_handler(a_connection).execute_change
+
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_profiles_autoincrement, l_parameters))
+ db_handler(a_connection).execute_change
+
+ a_connection.commit
+ end
+
+
+
+feature -- Database Hanlder
+
+ db_handler (a_connection: DATABASE_CONNECTION): DATABASE_HANDLER
+ -- Db handler
+ once
+ create {DATABASE_HANDLER_IMPL} Result.make (a_connection)
+ end
+
+
+feature -- Sql delete queries
+
+ Sql_delete_users: STRING = "delete from Users"
+ -- Clean Users.
+
+ Sql_delete_nodes: STRING = "delete from Nodes"
+ -- Clean Nodes.
+
+ Sql_delete_roles: STRING = "delete from Roles"
+ -- Clean Roles.
+
+ Sql_delete_permissions: STRING = "delete from Permissions"
+ -- Clean Permissions.
+
+ Sql_delete_users_roles: STRING = "delete from Users_roles"
+ -- Clean User roles.
+
+ Sql_delete_user_profiles: STRING = "delete from profiles"
+ -- Clean profiles.
+
+ Sql_delete_users_nodes: STRING = "delete from users_nodes"
+
+ Rest_users_autoincrement: STRING = "ALTER TABLE Users AUTO_INCREMENT = 1"
+ -- reset autoincrement
+
+ Rest_nodes_autoincrement: STRING = "ALTER TABLE Nodes AUTO_INCREMENT = 1"
+ -- reset autoincrement.
+
+ Rest_roles_autoincrement: STRING = "ALTER TABLE Roles AUTO_INCREMENT = 1"
+ -- reset autoincrement.
+
+ Rest_permissions_autoincrement: STRING = "ALTER TABLE Permissions AUTO_INCREMENT = 1"
+ -- reset autoincrement.
+
+ Rest_profiles_autoincrement: STRING = "ALTER TABLE Profiles AUTO_INCREMENT = 1"
+ -- reset autoincrement.
+
+
+
+end
diff --git a/library/persistence/store_odbc/src/cms_storage_store_odbc.e b/library/persistence/store_odbc/src/cms_storage_store_odbc.e
new file mode 100644
index 0000000..1fa9922
--- /dev/null
+++ b/library/persistence/store_odbc/src/cms_storage_store_odbc.e
@@ -0,0 +1,82 @@
+note
+ description: "Summary description for {CMS_STORAGE_STORE_ODBC}."
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_STORAGE_STORE_ODBC
+
+inherit
+ CMS_STORAGE_STORE_SQL
+ redefine
+ make
+ end
+
+ CMS_CORE_STORAGE_SQL_I
+
+ CMS_USER_STORAGE_SQL_I
+
+ REFACTORING_HELPER
+
+create
+ make,
+ make_with_driver
+
+feature {NONE} -- Initialization
+
+ make (a_connection: DATABASE_CONNECTION)
+ --
+ do
+ Precursor (a_connection)
+ create odbc_driver.make_from_string_general ("odbc")
+ end
+
+ make_with_driver (a_connection: DATABASE_CONNECTION; a_driver: detachable READABLE_STRING_GENERAL)
+ require
+ is_connected: a_connection.is_connected
+ do
+ make (a_connection)
+ if a_driver /= Void then
+ create odbc_driver.make_from_string_general (a_driver)
+ end
+ end
+
+feature -- Status report
+
+ odbc_driver: IMMUTABLE_STRING_32
+ -- Database's driver name.
+ -- sqlite, mysql, ...
+
+ is_initialized: BOOLEAN
+ -- Is storage initialized?
+ do
+ Result := has_user
+ end
+
+feature -- Conversion
+
+ sql_statement (a_statement: STRING): STRING
+ -- .
+ local
+ i: INTEGER
+ do
+ Result := a_statement
+ if odbc_driver.same_caseless_characters_general ("sqlite3", 1, 5, 1) then
+ from
+ i := 1
+ until
+ i = 0
+ loop
+ i := a_statement.substring_index ("AUTO_INCREMENT", i)
+ if i > 0 then
+ if Result = a_statement then
+ create Result.make_from_string (a_statement)
+ end
+ Result.remove (i + 4)
+ i := i + 14
+ end
+ end
+ end
+ end
+
+end
diff --git a/library/persistence/store_odbc/src/cms_storage_store_odbc_builder.e b/library/persistence/store_odbc/src/cms_storage_store_odbc_builder.e
new file mode 100644
index 0000000..9deb926
--- /dev/null
+++ b/library/persistence/store_odbc/src/cms_storage_store_odbc_builder.e
@@ -0,0 +1,71 @@
+note
+ description: "[
+ Interface responsible to instantiate CMS_STORAGE_STORE_ODBC object.
+ ]"
+ author: "$Author: jfiat $"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_STORAGE_STORE_ODBC_BUILDER
+
+inherit
+ CMS_STORAGE_STORE_SQL_BUILDER
+
+ GLOBAL_SETTINGS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Initialize `Current'.
+ do
+ end
+
+feature -- Factory
+
+ storage (a_setup: CMS_SETUP; a_error_handler: ERROR_HANDLER): detachable CMS_STORAGE_STORE_ODBC
+ local
+ s: detachable STRING
+ conn: detachable DATABASE_CONNECTION
+ do
+ if
+ attached (create {APPLICATION_JSON_CONFIGURATION_HELPER}).new_database_configuration (a_setup.environment.application_config_path) as l_database_config
+ then
+ if l_database_config.driver.is_case_insensitive_equal ("odbc") then
+ s := l_database_config.database_string
+ if attached reuseable_connection.item as d then
+ if s.same_string (d.name) then
+ conn := d.connection
+ end
+ end
+ if conn = Void or else not conn.is_connected then
+ create {DATABASE_CONNECTION_ODBC} conn.login_with_connection_string (s)
+ reuseable_connection.replace ([s, conn])
+ end
+ if conn.is_connected then
+ create Result.make_with_driver (conn, l_database_config.item ("Driver"))
+ set_map_zero_null_value (False) --| This way we map 0 to 0, instead of Null as default.
+ set_use_extended_types (True) --| Use extended types: STRING_32 etc.
+ if Result.is_available then
+ if not Result.is_initialized then
+ initialize (a_setup, Result)
+ end
+ end
+ else
+ a_error_handler.add_custom_error (0, "Could not connect to the ODBC storage", Void)
+ end
+ else
+ -- Wrong mapping between storage name and storage builder!
+ end
+ end
+ end
+
+ reuseable_connection: CELL [detachable TUPLE [name: STRING; connection: DATABASE_CONNECTION]]
+ once
+ create Result.put (Void)
+ end
+
+end
diff --git a/library/persistence/store_odbc/store_odbc-safe.ecf b/library/persistence/store_odbc/store_odbc-safe.ecf
new file mode 100644
index 0000000..e06a4d0
--- /dev/null
+++ b/library/persistence/store_odbc/store_odbc-safe.ecf
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
diff --git a/library/persistence/store_odbc/tests/Readme.md b/library/persistence/store_odbc/tests/Readme.md
new file mode 100644
index 0000000..b81d64f
--- /dev/null
+++ b/library/persistence/store_odbc/tests/Readme.md
@@ -0,0 +1,26 @@
+SQLite ODBC.
+
+Install the odbc driver from http://www.ch-werner.de/sqliteodbc/
+
+Current version
+ sqliteodbc.exe -- Win32
+ sqliteodbc_w64.exe -- Win64
+
+
+Test the ODBC driver using Firefox SQLite DBManager https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/
+
+1. Open the odbc manger from Windows, create a new database using the SQLite3 ODBC driver and then open it from Firefox.
+
+
+EiffelStore + SQLiteODBC.
+
+Connection String: https://www.connectionstrings.com/sqlite3-odbc-driver/
+
+ "Driver=SQLite3 ODBC Driver;Database=./cms_lite.db;LongNames=0;Timeout=1000;NoTXN=0;SyncPragma=NORMAL;StepAPI=0;"
+
+Edit the database location based on your system.
+
+
+
+
+
diff --git a/library/persistence/store_odbc/tests/application.e b/library/persistence/store_odbc/tests/application.e
new file mode 100644
index 0000000..2b87dd6
--- /dev/null
+++ b/library/persistence/store_odbc/tests/application.e
@@ -0,0 +1,29 @@
+note
+ description : "tests application root class"
+ date : "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision : "$Revision: 96542 $"
+
+class
+ APPLICATION
+
+inherit
+ ARGUMENTS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Run application.
+ local
+ storage: CMS_STORAGE_STORE_ODBC
+ do
+ -- Change the path.
+ create connection.login_with_connection_string ("Driver=SQLite3 ODBC Driver;Database=./cms_lite.db;LongNames=0;Timeout=1000;NoTXN=0;SyncPragma=NORMAL;StepAPI=0;")
+ create storage.make_with_driver (connection, "sqlite3")
+ end
+
+ connection: DATABASE_CONNECTION_ODBC
+
+end
diff --git a/library/persistence/store_odbc/tests/cms_lite.db b/library/persistence/store_odbc/tests/cms_lite.db
new file mode 100644
index 0000000..208c3a4
Binary files /dev/null and b/library/persistence/store_odbc/tests/cms_lite.db differ
diff --git a/library/persistence/store_odbc/tests/nodes/node_test_set.e b/library/persistence/store_odbc/tests/nodes/node_test_set.e
new file mode 100644
index 0000000..e80f720
--- /dev/null
+++ b/library/persistence/store_odbc/tests/nodes/node_test_set.e
@@ -0,0 +1,205 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ testing: "type/manual"
+
+class
+ NODE_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ assert ("Empty Nodes", storage.nodes.after)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_new_node
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ storage.new_node (default_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached storage.node_by_id (1))
+ -- Not exist node with id 2
+ assert ("Not exist node with id 2", storage.node_by_id (2) = Void)
+ end
+
+
+ test_update_node
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node (content and summary)
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ l_un.set_content ("Updating test node udpate ")
+ l_un.set_summary ("updating summary")
+ storage.update_node (l_un)
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_node.content) and then not (ll_node.summary ~ l_node.summary) and then ll_node.title ~ l_node.title )
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title )
+ end
+
+ -- Update node (content and summary and title)
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ l_un.set_content ("Updating test node udpate ")
+ l_un.set_summary ("updating summary")
+ l_un.set_title ("Updating Test case")
+ storage.update_node (l_un)
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_node.content) and then not (ll_node.summary ~ l_node.summary) and then not (ll_node.title ~ l_node.title) )
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title )
+ end
+ end
+
+ test_update_title
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node title
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_title (1, l_un.id, "New Title")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then ll_node.summary ~ l_un.summary and then not ( ll_node.title ~ l_un.title) and then ll_node.title ~ "New Title" )
+ end
+ end
+
+ test_update_summary
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node summary
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_summary (1, l_un.id,"New Summary")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_un.content and then not (ll_node.summary ~ l_un.summary) and then ll_node.summary ~ "New Summary" and then ll_node.title ~ l_un.title)
+ end
+ end
+
+ test_update_content
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Update node content
+
+ if attached {CMS_NODE} storage.node_by_id (1) as l_un then
+ storage.update_node_content (1, l_un.id, "New Content")
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then not (ll_node.content ~ l_un.content) and then ll_node.content ~ "New Content" and then ll_node.summary ~ l_un.summary and then ll_node.title ~ l_un.title)
+ end
+ end
+
+
+ test_delete_node
+ local
+ l_node: CMS_NODE
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ l_node := custom_node (" test node udpate ", "Update node", "Test case update")
+ storage.new_node (l_node)
+ assert ("Not empty Nodes after new_node", not storage.nodes.after)
+ -- Exist node with id 1
+ assert ("Exist node with id 1", attached {CMS_NODE} storage.node_by_id (1) as ll_node and then ll_node.content ~ l_node.content and then ll_node.summary ~ l_node.summary and then ll_node.title ~ l_node.title )
+
+ -- Delte node 1
+
+ storage.delete_node_by_id (1)
+ assert ("Node does not exist", storage.node_by_id (1) = Void)
+ end
+
+ test_recent_nodes
+ -- Content_10, Summary_10, Title_10
+ -- Content_9, Summary_9, Title_9
+ -- ..
+ -- Content_1, Summary_1, Title_1
+ local
+ i : INTEGER
+ do
+ assert ("Empty Nodes", storage.nodes.after)
+ across 1 |..| 10 as c loop
+ storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+ end
+
+ -- Scenario (0,10) rows, recents (10 down to 1)
+ i := 10
+ across storage.recent_nodes (0, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenario (5, 10) rows, recent nodes (5 down to 1)
+ i := 5
+ across storage.recent_nodes (5, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenario (9,10) rows, recent node 1
+ i := 1
+ across storage.recent_nodes (9, 10) as c loop
+ assert ("Same id:" + i.out, c.item.id = i)
+ i := i - 1
+ end
+
+ -- Scenrario 10..10 empty
+ assert ("Empty", storage.recent_nodes (10, 10).after)
+
+ end
+
+
+end
+
+
+
diff --git a/library/persistence/store_odbc/tests/storage/storage_test_set.e b/library/persistence/store_odbc/tests/storage/storage_test_set.e
new file mode 100644
index 0000000..0189001
--- /dev/null
+++ b/library/persistence/store_odbc/tests/storage/storage_test_set.e
@@ -0,0 +1,273 @@
+note
+ description: "Summary description for {STORAGE_TEST_SET}."
+ author: ""
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ STORAGE_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_has_user
+ do
+ assert ("Not has user", not storage.has_user)
+ end
+
+ test_all_users
+ do
+ assert ("to implement all_users", False)
+ end
+
+ test_user_by_id_not_exist
+ do
+ assert ("User does not exist", storage.user_by_id (1) = Void)
+ end
+
+ test_user_by_name_not_exist
+ do
+ assert ("User does not exist", storage.user_by_name ("test") = Void)
+ end
+
+ test_user_by_email_not_exist
+ do
+ assert ("User does not exist", storage.user_by_name ("test@test.com") = Void)
+ end
+
+ test_user_with_bad_id
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_id (0)
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_user_with_bad_name_empty
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_name ("")
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_user_with_bad_email_empty
+ local
+ l_retry: BOOLEAN
+ l_user: detachable CMS_USER
+ do
+ if not l_retry then
+ l_user := storage.user_by_email ("")
+ assert ("Precondition does not get the wrong value", False)
+ else
+ assert ("Expected precondition violation", True)
+ end
+ rescue
+ l_retry := True
+ retry
+ end
+
+ test_save_user
+ do
+ storage.save_user (default_user)
+ assert ("Has user", storage.has_user)
+ end
+
+ test_user_by_id
+ do
+ storage.save_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_id (1) as l_user then
+ assert ("Exist", True)
+ assert ("User test", l_user.name ~ "test")
+ assert ("User id = 1", l_user.id = 1)
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_user_by_name
+ do
+ storage.save_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_name ("test") as l_user then
+ assert ("Exist", True)
+ assert ("User nane: test", l_user.name ~ "test")
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_user_by_email
+ do
+ storage.save_user (default_user)
+ assert ("Has user", storage.has_user)
+ if attached {CMS_USER} storage.user_by_email ("test@test.com") as l_user then
+ assert ("Exist", True)
+ assert ("User email: test@test.com", l_user.email ~ "test@test.com")
+ else
+ assert ("Wrong Implementation", False)
+ end
+ end
+
+ test_invalid_credential
+ do
+ storage.save_user (default_user)
+ assert ("Has user test", attached storage.user_by_name ("test"))
+ assert ("Wrong password", not storage.is_valid_credential ("test", "test"))
+ assert ("Wrong user", not storage.is_valid_credential ("test1", "test"))
+ end
+
+ test_valid_credential
+ do
+ storage.save_user (default_user)
+ assert ("Has user test", attached storage.user_by_name ("test"))
+ assert ("Valid password", storage.is_valid_credential ("test", "password"))
+ end
+
+-- test_recent_nodes_empty
+-- do
+-- assert ("No recent nodes", storage.recent_nodes (0, 10).is_empty)
+-- end
+
+-- test_recent_nodes
+-- local
+-- l_nodes: LIST[CMS_NODE]
+-- do
+-- across 1 |..| 10 as c loop
+-- storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+-- end
+-- l_nodes := storage.recent_nodes (0, 10)
+-- assert ("10 recent nodes", l_nodes.count = 10)
+-- assert ("First node id=10", l_nodes.first.id = 10)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+
+-- l_nodes := storage.recent_nodes (5, 10)
+-- assert ("5 recent nodes", l_nodes.count = 5)
+-- assert ("First node id=5", l_nodes.first.id = 5)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+-- l_nodes := storage.recent_nodes (9, 10)
+-- assert ("1 recent nodes", l_nodes.count = 1)
+-- assert ("First node id=1", l_nodes.first.id = 1)
+-- assert ("Last node id=1", l_nodes.last.id = 1)
+
+-- l_nodes := storage.recent_nodes (10, 10)
+-- assert ("Is empty", l_nodes.is_empty)
+-- end
+
+-- test_node_does_not_exist
+-- do
+-- across 1 |..| 10 as c loop
+-- storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+-- end
+-- assert ("Not exist node id: 12", storage.node_by_id (12) = Void)
+-- end
+
+-- test_node
+-- do
+-- across 1 |..| 10 as c loop
+-- storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+-- end
+-- assert ("has nodes", storage.nodes.count > 5)
+-- assert ("Node id: 10", attached storage.node_by_id (10) as l_node and then l_node.title ~ "Title_10" )
+-- end
+
+-- test_update_node
+-- local
+-- l_node: CMS_NODE
+-- do
+-- storage.new_node (custom_node ("Content", "Summary", "Title"))
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+-- l_node := ll_node.twin
+-- l_node.set_content ("New Content")
+-- l_node.set_summary ("New Summary")
+-- l_node.set_title("New Title")
+
+---- storage.update_node (l_node)
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then not (u_node.content ~ ll_node.content) and then not (u_node.summary ~ ll_node.summary))
+-- end
+-- end
+
+-- test_update_node_title
+-- do
+-- storage.new_node (custom_node ("Content", "Summary", "Title"))
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+---- storage.update_node_title (ll_node.id, "New Title")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then not (u_node.title ~ ll_node.title) and then u_node.content ~ ll_node.content and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+-- test_update_node_summary
+-- do
+-- storage.new_node (custom_node ("Content", "Summary", "Title"))
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+---- storage.update_node_summary (ll_node.id, "New Summary")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then u_node.content ~ ll_node.content and then not (u_node.summary ~ ll_node.summary))
+-- end
+-- end
+
+-- test_update_node_content
+-- do
+-- storage.new_node (custom_node ("Content", "Summary", "Title"))
+-- if attached {CMS_NODE} storage.node_by_id (1) as ll_node then
+---- storage.update_node_content (ll_node.id, "New Content")
+-- assert ("Updated", attached {CMS_NODE} storage.node_by_id (1) as u_node and then u_node.title ~ ll_node.title and then not (u_node.content ~ ll_node.content) and then u_node.summary ~ ll_node.summary)
+-- end
+-- end
+
+-- test_delete_node
+-- do
+-- across 1 |..| 10 as c loop
+-- storage.new_node (custom_node ("Content_" + c.item.out, "Summary_" + c.item.out, "Title_" + c.item.out))
+-- end
+-- assert ("Exist node id: 10", attached storage.node_by_id (10) as l_node and then l_node.title ~ "Title_10" )
+-- storage.delete_node_by_id (10)
+-- assert ("Not exist node id: 10", storage.node_by_id (10) = Void)
+-- end
+
+
+end
diff --git a/library/persistence/store_odbc/tests/tests-safe.ecf b/library/persistence/store_odbc/tests/tests-safe.ecf
new file mode 100644
index 0000000..12a9b9d
--- /dev/null
+++ b/library/persistence/store_odbc/tests/tests-safe.ecf
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+ /nodes$
+
+
+
+
diff --git a/library/persistence/store_odbc/tests/users/user_test_set.e b/library/persistence/store_odbc/tests/users/user_test_set.e
new file mode 100644
index 0000000..db5047c
--- /dev/null
+++ b/library/persistence/store_odbc/tests/users/user_test_set.e
@@ -0,0 +1,72 @@
+note
+ description: "[
+ Eiffel tests that can be executed by testing tool.
+ ]"
+ author: "EiffelStudio test wizard"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+ testing: "type/manual"
+
+class
+ USER_TEST_SET
+
+inherit
+ EQA_TEST_SET
+ redefine
+ on_prepare,
+ on_clean
+ select
+ default_create
+ end
+ ABSTRACT_DB_TEST
+ rename
+ default_create as default_db_test
+ end
+
+
+feature {NONE} -- Events
+
+ on_prepare
+ --
+ do
+ (create {CLEAN_DB}).clean_db(connection)
+ storage.new_user (custom_user ("admin", "admin","admin@admin.com"))
+ end
+
+ on_clean
+ --
+ do
+ end
+
+feature -- Test routines
+
+ test_user_exist
+ -- User admin exist
+ do
+ assert ("Not void", attached storage.user_by_email ("admin@admin.com"))
+ assert ("Not void", attached storage.user_by_id (1))
+ assert ("Not void", attached storage.user_by_name ("admin"))
+ end
+
+ test_user_not_exist
+ -- Uset test does not exist.
+ do
+ assert ("Void", storage.user_by_email ("test@admin.com") = Void)
+ assert ("Void", storage.user_by_id (2) = Void )
+ assert ("Void", storage.user_by_name ("test") = Void)
+ end
+
+ test_new_user
+ do
+ storage.new_user (custom_user ("test", "test","test@admin.com"))
+ assert ("Not void", attached storage.user_by_email ("test@admin.com"))
+ assert ("Not void", attached storage.user_by_id (2))
+ assert ("Not void", attached storage.user_by_id (2) as l_user and then l_user.id = 2 and then l_user.name ~ "test")
+ assert ("Not void", attached storage.user_by_name ("test"))
+ end
+
+
+
+end
+
+
diff --git a/library/persistence/store_odbc/tests/util/abstract_db_test.e b/library/persistence/store_odbc/tests/util/abstract_db_test.e
new file mode 100644
index 0000000..d6177d3
--- /dev/null
+++ b/library/persistence/store_odbc/tests/util/abstract_db_test.e
@@ -0,0 +1,55 @@
+note
+ description: "Summary description for {ABSTRACT_DB_TEST}."
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+class
+ ABSTRACT_DB_TEST
+
+
+feature -- Database connection
+
+ connection: DATABASE_CONNECTION_ODBC
+ -- odbc database connection
+ once
+ create Result.login_with_connection_string ("Driver=SQLite3 ODBC Driver;Database=test_roc.db;LongNames=0;Timeout=1000;NoTXN=0;SyncPragma=NORMAL;StepAPI=0;")
+-- create Result.make_basic ("test_roc.db")
+ end
+
+feature {NONE} -- Implementation
+
+ storage: CMS_STORAGE
+ -- node provider.
+ once
+ create {CMS_STORAGE_STORE_ODBC} Result.make_with_driver (connection, "sqlite3")
+ end
+
+feature {NONE} -- Fixture Factory: Users
+
+ default_user: CMS_USER
+ do
+ Result := custom_user ("test", "password", "test@test.com")
+ end
+
+ custom_user (a_name, a_password, a_email: READABLE_STRING_32): CMS_USER
+ do
+ create Result.make (a_name)
+ Result.set_password (a_password)
+ Result.set_email (a_email)
+ end
+
+--feature {NONE} -- Fixture Factories: Nodes
+
+-- default_node: CMS_NODE
+-- do
+-- Result := custom_node ("Default content", "default summary", "Default")
+-- end
+
+-- custom_node (a_content, a_summary, a_title: READABLE_STRING_32): CMS_PAGE
+-- do
+-- create Result.make (a_title)
+-- Result.set_content (a_content, a_summary, Void)
+-- end
+
+
+end
diff --git a/library/persistence/store_odbc/tests/util/clean_db.e b/library/persistence/store_odbc/tests/util/clean_db.e
new file mode 100644
index 0000000..d381540
--- /dev/null
+++ b/library/persistence/store_odbc/tests/util/clean_db.e
@@ -0,0 +1,66 @@
+note
+ description: "[
+ Setting up database tests
+ 1. Put the database in a known state before running your test suite.
+ 2. Data reinitialization. For testing in developer sandboxes, something that you should do every time you rebuild the system, you may want to forgo dropping and rebuilding the database in favor of simply reinitializing the source data.
+ You can do this either by erasing all existing data and then inserting the initial data vales back into the database, or you can simple run updates to reset the data values.
+ The first approach is less risky and may even be faster for large amounts of data. - See more at: http://www.agiledata.org/essays/databaseTesting.html#sthash.6yVp35g8.dpuf
+ ]"
+
+ date: "$Date$"
+ revision: "$Revision$"
+ EIS: "name=Database Testing", "src=http://www.agiledata.org/essays/databaseTesting.html", "protocol=uri"
+
+class
+ CLEAN_DB
+
+feature
+
+ clean_db (a_connection: DATABASE_CONNECTION)
+ -- Clean db test.
+ local
+ l_parameters: STRING_TABLE[STRING]
+ do
+ create l_parameters.make (0)
+
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_nodes, l_parameters))
+ db_handler(a_connection).execute_change
+
+ -- Clean Users
+ db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Sql_delete_users, l_parameters))
+ db_handler(a_connection).execute_change
+
+-- -- Reset Autoincremente
+-- db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_users_autoincrement, l_parameters))
+-- db_handler(a_connection).execute_change
+
+-- db_handler(a_connection).set_query (create {DATABASE_QUERY}.data_reader (Rest_nodes_autoincrement, l_parameters))
+-- db_handler(a_connection).execute_change
+ end
+
+
+
+feature -- Database Hanlder
+
+ db_handler (a_connection: DATABASE_CONNECTION): DATABASE_HANDLER
+ -- Db handler
+ once
+ create {DATABASE_HANDLER_IMPL} Result.make (a_connection)
+ end
+
+
+feature -- Sql delete queries
+
+ Sql_delete_users: STRING = "delete from Users"
+ -- Clean Users.
+
+ Sql_delete_nodes: STRING = "delete from Nodes"
+ -- Clean Nodes.
+
+ Rest_users_autoincrement: STRING = "ALTER TABLE Users AUTO_INCREMENT = 1"
+ -- reset autoincrement
+
+ Rest_nodes_autoincrement: STRING = "ALTER TABLE Nodes AUTO_INCREMENT = 1"
+ -- reset autoincrement.
+
+end
diff --git a/license.lic b/license.lic
new file mode 100644
index 0000000..3e50931
--- /dev/null
+++ b/license.lic
@@ -0,0 +1,3 @@
+${NOTE_KEYWORD}
+ copyright: "2011-${YEAR}, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
diff --git a/modules/README.md b/modules/README.md
new file mode 100644
index 0000000..167ed85
--- /dev/null
+++ b/modules/README.md
@@ -0,0 +1 @@
+Location for contributed CMS modules.
diff --git a/modules/admin/admin-safe.ecf b/modules/admin/admin-safe.ecf
new file mode 100644
index 0000000..033c78b
--- /dev/null
+++ b/modules/admin/admin-safe.ecf
@@ -0,0 +1,28 @@
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/admin/cms_admin_module.e b/modules/admin/cms_admin_module.e
new file mode 100644
index 0000000..e3ece0e
--- /dev/null
+++ b/modules/admin/cms_admin_module.e
@@ -0,0 +1,162 @@
+note
+ description: "CMS module providing Administration support (back-end)."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_MODULE
+
+inherit
+ CMS_MODULE
+ redefine
+ setup_hooks,
+ permissions
+ end
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_RESPONSE_ALTER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create Current module, disabled by default.
+ do
+ version := "1.0"
+ description := "Service to Administrate CMS (users, modules, etc)"
+ package := "core"
+ end
+
+feature -- Access
+
+ name: STRING = "admin"
+
+feature {CMS_API} -- Module Initialization
+
+feature -- Access: router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ configure_web (a_api, a_router)
+ end
+
+ configure_web (a_api: CMS_API; a_router: WSF_ROUTER)
+ local
+ l_admin_handler: CMS_ADMIN_HANDLER
+
+ l_modules_handler: CMS_ADMIN_MODULES_HANDLER
+ l_users_handler: CMS_ADMIN_USERS_HANDLER
+ l_roles_handler: CMS_ADMIN_ROLES_HANDLER
+
+ l_user_handler: CMS_USER_HANDLER
+ l_role_handler: CMS_ROLE_HANDLER
+
+ l_admin_cache_handler: CMS_ADMIN_CACHE_HANDLER
+ l_admin_export_handler: CMS_ADMIN_EXPORT_HANDLER
+
+ l_uri_mapping: WSF_URI_MAPPING
+ do
+ create l_admin_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin", l_admin_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_modules_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin/modules", l_modules_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_users_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin/users", l_users_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_roles_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin/roles", l_roles_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_admin_cache_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin/cache", l_admin_cache_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_admin_export_handler.make (a_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/admin/export", l_admin_export_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ create l_user_handler.make (a_api)
+ a_router.handle ("/admin/add/user", l_user_handler, a_router.methods_get_post)
+ a_router.handle ("/admin/user/{id}", l_user_handler, a_router.methods_get)
+ a_router.handle ("/admin/user/{id}/edit", l_user_handler, a_router.methods_get_post)
+ a_router.handle ("/admin/user/{id}/delete", l_user_handler, a_router.methods_get_post)
+
+ create l_role_handler.make (a_api)
+ a_router.handle ("/admin/add/role", l_role_handler, a_router.methods_get_post)
+ a_router.handle ("/admin/role/{id}", l_role_handler, a_router.methods_get)
+ a_router.handle ("/admin/role/{id}/edit", l_role_handler, a_router.methods_get_post)
+ a_router.handle ("/admin/role/{id}/delete", l_role_handler, a_router.methods_get_post)
+ end
+
+feature -- Security
+
+ permissions: LIST [READABLE_STRING_8]
+ -- List of permission ids, used by this module, and declared.
+ do
+ Result := Precursor
+ Result.force ("manage admin")
+ Result.force ("admin users")
+ Result.force ("admin roles")
+ Result.force ("admin modules")
+ Result.force ("install modules")
+ Result.force ("admin core caches")
+ Result.force ("clear blocks cache")
+ Result.force ("admin export")
+ Result.force ("export core")
+ end
+
+feature -- Hooks
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ --
+ do
+ a_hooks.subscribe_to_menu_system_alter_hook (Current)
+ a_hooks.subscribe_to_response_alter_hook (Current)
+ end
+
+ response_alter (a_response: CMS_RESPONSE)
+ --
+ do
+ a_response.add_style (a_response.url ("/module/" + name + "/files/css/admin.css", Void), Void)
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ local
+ lnk: CMS_LOCAL_LINK
+ do
+ if
+ a_response.has_permission ("manage " + {CMS_ADMIN_MODULE}.name) -- Note: admin user has all permissions enabled by default.
+ then
+ -- TODO: we should probably use more side menu and less primary_menu.
+ create lnk.make ("Admin", "admin")
+ lnk.set_permission_arguments (<<"manage " + {CMS_ADMIN_MODULE}.name>>)
+ a_menu_system.management_menu.extend (lnk)
+
+ end
+
+ create lnk.make ("Module", "admin/modules")
+ lnk.set_permission_arguments (<<"manage module">>)
+ a_menu_system.management_menu.extend (lnk)
+
+ -- Per module cache permission!
+ create lnk.make ("Cache", "admin/cache")
+ a_menu_system.management_menu.extend (lnk)
+
+ -- Per module export permission!
+ create lnk.make ("Export", "admin/export")
+ a_menu_system.management_menu.extend (lnk)
+ end
+
+note
+ copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/modules/admin/handler/cms_admin_cache_handler.e b/modules/admin/handler/cms_admin_cache_handler.e
new file mode 100644
index 0000000..5f53515
--- /dev/null
+++ b/modules/admin/handler/cms_admin_cache_handler.e
@@ -0,0 +1,93 @@
+note
+ description: "[
+ Administrate cache functionality.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_CACHE_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Execution
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+ f := clear_cache_web_form (l_response)
+ create s.make_empty
+ f.append_to_html (l_response.wsf_theme, s)
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+ f := clear_cache_web_form (l_response)
+ f.process (l_response)
+ if
+ attached f.last_data as fd and then
+ fd.is_valid
+ then
+ if attached fd.string_item ("op") as l_op and then l_op.same_string (text_clear_all_caches) then
+ api.hooks.invoke_clear_cache (Void, l_response)
+ l_response.add_notice_message ("Caches cleared (if allowed)!")
+ else
+ fd.report_error ("Invalid form data!")
+ end
+ end
+ create s.make_empty
+ f.append_to_html (l_response.wsf_theme, s)
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+
+feature -- Widget
+
+ clear_cache_web_form (a_response: CMS_RESPONSE): CMS_FORM
+ local
+ but: WSF_FORM_SUBMIT_INPUT
+ do
+ create Result.make (a_response.url (a_response.location, Void), "form_clear_cache")
+ create but.make_with_text ("op", text_clear_all_caches)
+ Result.extend (but)
+ end
+
+feature -- Interface text.
+
+ text_clear_all_caches: STRING_32 = "Clear all caches"
+
+end
diff --git a/modules/admin/handler/cms_admin_export_handler.e b/modules/admin/handler/cms_admin_export_handler.e
new file mode 100644
index 0000000..8fd510e
--- /dev/null
+++ b/modules/admin/handler/cms_admin_export_handler.e
@@ -0,0 +1,115 @@
+note
+ description: "[
+ Administrate export functionality.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_EXPORT_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Execution
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+ f := exportation_web_form (l_response)
+ create s.make_empty
+ f.append_to_html (l_response.wsf_theme, s)
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ l_exportation_parameters: CMS_EXPORT_PARAMETERS
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+ f := exportation_web_form (l_response)
+ f.process (l_response)
+ if
+ attached f.last_data as fd and then
+ fd.is_valid
+ then
+ if attached fd.string_item ("op") as l_op and then l_op.same_string (text_export_all_data) then
+ if attached fd.string_item ("folder") as l_folder then
+ create l_exportation_parameters.make (api.site_location.extended ("export").extended (l_folder))
+ else
+ create l_exportation_parameters.make (api.site_location.extended ("export").extended ((create {DATE_TIME}.make_now_utc).formatted_out ("yyyy-[0]mm-[0]dd---hh24-[0]mi-[0]ss")))
+ end
+ api.hooks.invoke_export_to (Void, l_exportation_parameters, l_response)
+ l_response.add_notice_message ("All data exported (if allowed)!")
+ create s.make_empty
+ across
+ l_exportation_parameters.logs as ic
+ loop
+ s.append (ic.item)
+ s.append (" ")
+ s.append_character ('%N')
+ end
+ l_response.add_notice_message (s)
+ else
+ fd.report_error ("Invalid form data!")
+ end
+ end
+ create s.make_empty
+ f.append_to_html (l_response.wsf_theme, s)
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+
+feature -- Widget
+
+ exportation_web_form (a_response: CMS_RESPONSE): CMS_FORM
+ local
+ f_name: WSF_FORM_TEXT_INPUT
+ but: WSF_FORM_SUBMIT_INPUT
+ do
+ create Result.make (a_response.url (a_response.location, Void), "export_all_data")
+ Result.extend_raw_text ("Export CMS data to ")
+ create f_name.make_with_text ("folder", (create {DATE_TIME}.make_now_utc).formatted_out ("yyyy-[0]mm-[0]dd---hh24-[0]mi-[0]ss"))
+ f_name.set_label ("Export folder name")
+ f_name.set_description ("Folder name under 'exports' folder.")
+ f_name.set_is_required (True)
+ Result.extend (f_name)
+ create but.make_with_text ("op", text_export_all_data)
+ Result.extend (but)
+ end
+
+feature -- Interface text.
+
+ text_export_all_data: STRING_32 = "Export all data"
+
+end
diff --git a/modules/admin/handler/cms_admin_handler.e b/modules/admin/handler/cms_admin_handler.e
new file mode 100644
index 0000000..49d123b
--- /dev/null
+++ b/modules/admin/handler/cms_admin_handler.e
@@ -0,0 +1,89 @@
+note
+ description: "[
+ handler for CMS admin in the CMS interface.
+
+ TODO: implement REST API.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("manage " + {CMS_ADMIN_MODULE}.name) then
+ create {CMS_ADMIN_RESPONSE} r.make (req, res, api)
+ r.execute
+ else
+ r.execute
+ end
+ end
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("manage " + {CMS_ADMIN_MODULE}.name) then
+ create {CMS_ADMIN_RESPONSE} r.make (req, res, api)
+ r.execute
+ else
+ r.execute
+ end
+ end
+
+end
diff --git a/modules/admin/handler/cms_admin_modules_handler.e b/modules/admin/handler/cms_admin_modules_handler.e
new file mode 100644
index 0000000..07e9d38
--- /dev/null
+++ b/modules/admin/handler/cms_admin_modules_handler.e
@@ -0,0 +1,320 @@
+note
+ description: "[
+ Administrate modules.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_MODULES_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get, do_post
+ end
+
+ REFACTORING_HELPER
+
+ CMS_SETUP_ACCESS
+
+ CMS_ACCESS
+
+create
+ make
+
+feature -- Execution
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ l_denied: BOOLEAN
+ do
+ if
+ attached {WSF_STRING} req.query_parameter ("op") as l_op and then l_op.same_string ("uninstall") and then
+ attached {WSF_TABLE} req.query_parameter ("module_uninstallation") as tb
+ then
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if attached api.setup.string_8_item ("admin.installation_access") as l_access then
+ if l_access.is_case_insensitive_equal ("none") then
+ l_denied := True
+ elseif l_access.is_case_insensitive_equal ("permission") then
+ l_denied := not r.has_permission ("install modules")
+ end
+ else
+ l_denied := True
+ end
+ if l_denied then
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("You do not have permission to access CMS module uninstallation procedure!")
+ else
+ create s.make_empty
+ across
+ tb as ic
+ loop
+ if attached api.setup.modules.item_by_name (ic.item.string_representation) as l_module then
+ if api.is_module_installed (l_module) then
+ api.uninstall_module (l_module)
+ if api.is_module_installed (l_module) then
+ s.append ("ERROR: Module " + l_module.name + " failed to be uninstalled!
")
+ else
+ s.append ("Module " + l_module.name + " was successfully uninstalled.
")
+ end
+ else
+ s.append ("Module " + l_module.name + " is not installed.
")
+ end
+ end
+ end
+ s.append (r.link ("Back to modules management", r.location, Void))
+ r.set_main_content (s)
+ end
+ r.execute
+ else
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ f := modules_collection_web_form (r)
+ create s.make_empty
+ f.append_to_html (r.wsf_theme, s)
+ r.set_page_title ("Modules")
+ r.set_main_content (s)
+ r.execute
+ end
+ end
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ s: STRING
+ f: CMS_FORM
+ l_denied: BOOLEAN
+ do
+ if attached {WSF_STRING} req.item ("op") as l_op then
+ if l_op.same_string ("Install modules") then
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+
+ if attached api.setup.string_8_item ("admin.installation_access") as l_access then
+ if l_access.is_case_insensitive_equal ("none") then
+ l_denied := True
+ elseif l_access.is_case_insensitive_equal ("permission") then
+ l_denied := not r.has_permission ("install modules")
+ end
+ else
+ l_denied := True
+ end
+ if l_denied then
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("You do not have permission to access CMS module installation procedure!")
+ else
+ f := modules_collection_web_form (r)
+ if l_op.same_string ("Install modules") then
+ f.submit_actions.extend (agent on_installation_submit)
+ f.process (r)
+ elseif l_op.same_string ("uninstall") then
+ f.submit_actions.extend (agent on_uninstallation_submit)
+ f.process (r)
+ end
+ if
+ not attached f.last_data as l_data or else
+ not l_data.is_valid
+ then
+ r.add_error_message ("Error occurred.")
+ create s.make_empty
+ f.append_to_html (r.wsf_theme, s)
+ r.set_page_title ("Modules")
+ r.set_main_content (s)
+ else
+ r.add_notice_message ("Operation on module(s) succeeded.")
+ r.set_redirection (r.location)
+ end
+ end
+ r.execute
+ else
+ create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.execute
+ end
+ else
+ do_get (req, res)
+ end
+ end
+
+ modules_collection_web_form (a_response: CMS_RESPONSE): CMS_FORM
+ local
+ mod: CMS_MODULE
+ f_cb: WSF_FORM_CHECKBOX_INPUT
+ w_tb: WSF_WIDGET_TABLE
+ w_row: WSF_WIDGET_TABLE_ROW
+ w_item: WSF_WIDGET_TABLE_ITEM
+ w_submit: WSF_FORM_SUBMIT_INPUT
+ w_set: WSF_FORM_FIELD_SET
+
+ l_mods_to_install: ARRAYED_LIST [CMS_MODULE]
+ do
+ create Result.make (a_response.url (a_response.location, Void), "modules_collection")
+ create w_tb.make
+ w_tb.add_css_class ("modules_table")
+ create w_row.make (5)
+ create w_item.make_with_text ("Enabled ")
+ w_row.add_item (w_item)
+ create w_item.make_with_text ("Module")
+ w_row.add_item (w_item)
+ create w_item.make_with_text ("Version")
+ w_row.add_item (w_item)
+ create w_item.make_with_text ("Description")
+ w_row.add_item (w_item)
+ w_tb.add_head_row (w_row)
+
+ create l_mods_to_install.make (0)
+ across
+ a_response.api.setup.modules as ic
+ loop
+ mod := ic.item
+ if not a_response.api.is_module_installed (mod) then
+ l_mods_to_install.extend (mod)
+ else
+ create w_row.make (5)
+ create f_cb.make ("module_" + mod.name)
+ f_cb.set_text_value (mod.name)
+ f_cb.set_checked (mod.is_enabled)
+ f_cb.set_is_readonly (True)
+ create w_item.make_with_content (f_cb)
+ w_row.add_item (w_item)
+
+ create w_item.make_with_text (mod.name)
+ w_row.add_item (w_item)
+
+ create w_item.make_with_text (mod.version)
+ w_row.add_item (w_item)
+
+ if attached mod.description as l_desc then
+ create w_item.make_with_text (l_desc)
+ w_row.add_item (w_item)
+ else
+ create w_item.make_with_text ("")
+ w_row.add_item (w_item)
+ end
+ create w_item.make_with_text (a_response.link ("Uninstall", a_response.location + "?op=uninstall&module_uninstallation[]=" + mod.name, Void))
+ w_row.add_item (w_item)
+
+ w_tb.add_row (w_row)
+ end
+ end
+ create w_set.make
+ w_set.set_legend ("Installed modules")
+ w_set.extend (w_tb)
+-- create w_submit.make ("op")
+-- w_submit.set_text_value ("Save")
+-- w_set.extend (w_submit)
+ Result.extend (w_set)
+
+ Result.extend_html_text (" ")
+
+ if not l_mods_to_install.is_empty then
+ create w_tb.make
+ w_tb.add_css_class ("modules_table")
+ create w_row.make (3)
+ create w_item.make_with_text ("Install ")
+ w_row.add_item (w_item)
+ create w_item.make_with_text ("Module")
+ w_row.add_item (w_item)
+ create w_item.make_with_text ("Description")
+ w_row.add_item (w_item)
+ w_tb.add_head_row (w_row)
+ across
+ l_mods_to_install as ic
+ loop
+ mod := ic.item
+ create w_row.make (3)
+ create f_cb.make ("module_installation[" + mod.name + "]")
+ f_cb.set_text_value (mod.name)
+ create w_item.make_with_content (f_cb)
+ w_row.add_item (w_item)
+
+ create w_item.make_with_text (mod.name)
+ w_row.add_item (w_item)
+
+ if attached mod.description as l_desc then
+ create w_item.make_with_text (l_desc)
+ w_row.add_item (w_item)
+ else
+ create w_item.make_with_text ("")
+ w_row.add_item (w_item)
+ end
+ w_tb.add_row (w_row)
+ end
+ create w_set.make
+ w_set.set_legend ("Available modules for installation")
+ w_set.extend (w_tb)
+ create w_submit.make ("op")
+ w_submit.set_text_value ("Install modules")
+ w_set.extend (w_submit)
+ Result.extend (w_set)
+ end
+ end
+
+ on_installation_submit (fd: WSF_FORM_DATA)
+ local
+ l_mods: CMS_MODULE_COLLECTION
+ do
+ if attached {WSF_TABLE} fd.table_item ("module_installation") as tb and then not tb.is_empty then
+ l_mods := api.setup.modules
+ across
+ tb as ic
+ loop
+ if
+ attached {WSF_STRING} ic.item as l_mod_name and then
+ attached l_mods.item_by_name (l_mod_name.value) as m
+ then
+ api.install_module (m)
+ if not api.is_module_installed (m) then
+ fd.report_error ("Installation failed for module " + m.name)
+ end
+ else
+ fd.report_error ("Can not find associated module" + ic.item.as_string.url_encoded_value)
+ end
+ end
+ else
+ fd.report_error ("No module to install!")
+ end
+ end
+
+ on_uninstallation_submit (fd: WSF_FORM_DATA)
+ local
+ l_mods: CMS_MODULE_COLLECTION
+ do
+ if attached {WSF_TABLE} fd.table_item ("module_uninstallation") as tb and then not tb.is_empty then
+ l_mods := api.setup.modules
+ across
+ tb as ic
+ loop
+ if
+ attached {WSF_STRING} ic.item as l_mod_name and then
+ attached l_mods.item_by_name (l_mod_name.value) as m
+ then
+ api.uninstall_module (m)
+ if api.is_module_installed (m) then
+ fd.report_error ("Un-Installation failed for module " + m.name)
+ end
+ else
+ fd.report_error ("Can not find associated module" + ic.item.as_string.url_encoded_value)
+ end
+ end
+ else
+ fd.report_error ("No module to uninstall!")
+ end
+ end
+
+end
diff --git a/modules/admin/handler/cms_admin_response.e b/modules/admin/handler/cms_admin_response.e
new file mode 100644
index 0000000..fb7dea4
--- /dev/null
+++ b/modules/admin/handler/cms_admin_response.e
@@ -0,0 +1,39 @@
+note
+ description: "Summary description for {CMS_ADMIN_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_RESPONSE
+
+inherit
+ CMS_RESPONSE
+
+create
+ make
+
+feature -- Process
+
+ process
+ local
+ b: STRING
+ do
+ create b.make_empty
+ set_title (translation ("Admin Page", Void))
+ b.append ("")
+ fixme ("Check how to make it configurable")
+ if has_permissions (<< "admin users">>) then
+ b.append ("" + link ("Users", "admin/users", Void))
+ b.append ("View/Edit/Add Users
")
+ b.append (" ")
+ end
+ if has_permissions (<< "admin roles">>) then
+ b.append ("" + link ("Roles", "admin/roles", Void))
+ b.append ("View/Edit/Add Roles
")
+ b.append (" ")
+ end
+ b.append (" ")
+ set_main_content (b)
+ end
+
+end
diff --git a/modules/admin/handler/role/cms_admin_roles_handler.e b/modules/admin/handler/role/cms_admin_roles_handler.e
new file mode 100644
index 0000000..aeedba8
--- /dev/null
+++ b/modules/admin/handler/role/cms_admin_roles_handler.e
@@ -0,0 +1,112 @@
+note
+ description: "Summary description for {CMS_ADMIN_ROLE_HANDLER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_ROLES_HANDLER
+
+inherit
+
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ u: CMS_USER_ROLE
+ l_count: INTEGER
+ user_api: CMS_USER_API
+ do
+ -- At the moment the template are hardcoded, but we can
+ -- get them from the configuration file and load them into
+ -- the setup class.
+
+
+ user_api := api.user_api
+
+ l_count := user_api.roles_count
+
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+
+ create s.make_empty
+ if l_count > 1 then
+ l_response.set_title ("Listing " + l_count.out + " Roles")
+ else
+ l_response.set_title ("Listing " + l_count.out + " Role")
+ end
+
+ if attached user_api.roles as lst then
+ s.append ("%N")
+ end
+
+ if l_response.has_permission ("admin roles") then
+ s.append (l_response.link ("Add Role", "admin/add/role", Void))
+ end
+
+
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+end
+
diff --git a/modules/admin/handler/role/cms_role_form_response.e b/modules/admin/handler/role/cms_role_form_response.e
new file mode 100644
index 0000000..7cdec58
--- /dev/null
+++ b/modules/admin/handler/role/cms_role_form_response.e
@@ -0,0 +1,489 @@
+note
+ description: "Summary description for {CMS_ROLE_FORM_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ROLE_FORM_RESPONSE
+
+inherit
+ CMS_RESPONSE
+
+ CMS_SHARED_SORTING_UTILITIES
+
+create
+ make
+
+feature -- Query
+
+ role_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- Role id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- Process
+
+ process
+ -- Computed response message.
+ local
+ b: STRING_8
+ uid: INTEGER_64
+ user_api: CMS_USER_API
+ do
+ user_api := api.user_api
+ create b.make_empty
+ uid := role_id_path_parameter (request)
+ if uid > 0 and then attached user_api.user_role_by_id (uid.to_integer) as l_role then
+ fixme ("Issues with WSD_FORM_DATA.apply_to_associated_form")
+ -- if we have a WSF_FORM_CHECKBOK_INPUT, cheked inputs, are not preserverd in case of error.
+ if location.ends_with_general ("/edit") then
+ edit_form (l_role)
+ elseif location.ends_with_general ("/delete") then
+ delete_form (l_role)
+ end
+ else
+ new_form
+ end
+ end
+
+feature -- Process Edit
+
+ edit_form (a_role: CMS_USER_ROLE)
+ local
+ f: like new_edit_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ do
+ create b.make_empty
+ f := new_edit_form (a_role, url (request.percent_encoded_path_info, Void), "edit-user")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.validation_actions.extend (agent edit_form_validate(?,a_role, b))
+ f.submit_actions.extend (agent edit_form_submit(?, a_role, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_role.has_id then
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("View", Void), "admin/role/" + a_role.id.out), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void), "admin/role/" + a_role.id.out + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Delete", Void), "admin/role/" + a_role.id.out + "/delete"), primary_tabs)
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_role.name)
+ b.append (html_encoded (a_role.name) + " saved")
+ else
+ set_title (formatted_string (translation ("Edit $1 #$2", Void), [a_role.name, a_role.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+feature -- Process Delete
+
+ delete_form (a_role: CMS_USER_ROLE)
+ local
+ f: like new_delete_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ do
+ create b.make_empty
+ f := new_delete_form (a_role, url (request.percent_encoded_path_info, Void), "edit-user")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_role.has_id then
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("View", Void), "admin/role/" + a_role.id.out), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void), "admin/role/" + a_role.id.out + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Delete", Void), "admin/role/" + a_role.id.out + "/delete"), primary_tabs)
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_role.name)
+ b.append (html_encoded (a_role.name) + " deleted")
+ else
+ set_title (formatted_string (translation ("Delete $1 #$2", Void), [a_role.name, a_role.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+feature -- Process New
+
+ new_form
+ local
+ f: like new_edit_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ l_role: detachable CMS_USER_ROLE
+ do
+ create b.make_empty
+ f := new_edit_form (l_role, url (request.percent_encoded_path_info, Void), "create-role")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.validation_actions.extend (agent new_form_validate(?, b))
+ f.submit_actions.extend (agent edit_form_submit(?, l_role, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ if attached l_role then
+ set_title (l_role.name)
+ b.append (html_encoded (l_role.name) + " Saved")
+ end
+ else
+ if attached l_role then
+ set_title (formatted_string (translation ("Saved $1 #$2", Void), [l_role.name, l_role.id]))
+ end
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+feature -- Form
+
+ edit_form_submit (fd: WSF_FORM_DATA; a_role: detachable CMS_USER_ROLE; b: STRING)
+ local
+ l_save_role: BOOLEAN
+ l_update_role: BOOLEAN
+ do
+ l_save_role := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Create role")
+ if l_save_role then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ create_role (fd)
+ else
+ l_update_role := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Update role")
+ if l_update_role then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ if a_role /= Void then
+ update_role (fd, a_role)
+ else
+ fd.report_error ("Missing Role")
+ end
+ end
+ end
+ end
+
+ edit_form_validate (fd: WSF_FORM_DATA; a_role: CMS_USER_ROLE; b: STRING)
+ do
+ if attached fd.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Update role") then
+ if
+ attached fd.string_item ("role") as l_role and then
+ not a_role.name.is_case_insensitive_equal (l_role)
+ then
+ if attached api.user_api.user_role_by_name (l_role) then
+ fd.report_invalid_field ("role", "Role already taken!")
+ end
+ else
+ if fd.string_item ("role") = Void then
+ fd.report_invalid_field ("role", "missing role")
+ end
+ end
+ if attached {WSF_TABLE} fd.item ("new_cms_permissions[]") as l_perm then
+ a_role.permissions.compare_objects
+ across
+ l_perm.values as ic
+ loop
+ if attached {WSF_STRING} ic.item as p then
+ if not p.value.is_valid_as_string_8 then
+ fd.report_invalid_field ("new_cms_permissions[]", "Permission " + p.value + " should not have any unicode character!")
+ elseif across a_role.permissions as p_ic some p_ic.item.is_case_insensitive_equal_general (p.value) end then
+ fd.report_invalid_field ("new_cms_permissions[]", "Permission " + p.value + " already exists!")
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ new_edit_form (a_role: detachable CMS_USER_ROLE; a_url: READABLE_STRING_8; a_name: STRING;): CMS_FORM
+ -- Create a web form named `a_name' for uSER `a_YSER' (if set), using form action url `a_url'.
+ local
+ f: CMS_FORM
+ th: WSF_FORM_HIDDEN_INPUT
+ do
+ create f.make (a_url, a_name)
+ create th.make ("role-id")
+ if a_role /= Void then
+ th.set_text_value (a_role.id.out)
+ else
+ th.set_text_value ("0")
+ end
+ f.extend (th)
+ populate_form (f, a_role)
+ Result := f
+ end
+
+ new_form_validate (fd: WSF_FORM_DATA; b: STRING)
+ do
+ if attached fd.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Create role") then
+ if attached fd.string_item ("role") as l_role then
+ if attached api.user_api.user_role_by_name (l_role) then
+ fd.report_invalid_field ("role", "Role already taken!")
+ end
+ else
+ fd.report_invalid_field ("role", "missing role")
+ end
+ end
+ end
+ end
+
+ new_delete_form (a_role: detachable CMS_USER_ROLE; a_url: READABLE_STRING_8; a_name: STRING;): CMS_FORM
+ -- Create a web form named `a_name' for role `a_role' (if set), using form action url `a_url'.
+ local
+ f: CMS_FORM
+ ts: WSF_FORM_SUBMIT_INPUT
+ do
+ create f.make (a_url, a_name)
+ f.extend_html_text (" ")
+ f.extend_html_text ("Are you sure you want to delete? ")
+
+ -- TODO check if we need to check for has_permissions!!
+ if a_role /= Void and then a_role.has_id then
+ create ts.make ("op")
+ ts.set_default_value ("Delete")
+ fixme ("[
+ ts.set_default_value (translation ("Delete"))
+ ]")
+ f.extend (ts)
+ create ts.make ("op")
+ ts.set_default_value ("Cancel")
+ ts.set_formmethod ("GET")
+ ts.set_formaction ("/admin/role/" + a_role.id.out)
+ f.extend (ts)
+ end
+ Result := f
+ end
+
+ populate_form (a_form: WSF_FORM; a_role: detachable CMS_USER_ROLE)
+ -- Fill the web form `a_form' with data from `a_node' if set,
+ -- and apply this to content type `a_content_type'.
+ local
+ ti: WSF_FORM_TEXT_INPUT
+-- fe: WSF_FORM_EMAIL_INPUT
+ fs: WSF_FORM_FIELD_SET
+ cb: WSF_FORM_CHECKBOX_INPUT
+ ts: WSF_FORM_SUBMIT_INPUT
+-- tb: WSF_FORM_BUTTON_INPUT
+ lab: WSF_WIDGET_TEXT
+ l_role_permissions: detachable LIST [READABLE_STRING_8]
+ l_module_names: ARRAYED_LIST [READABLE_STRING_8]
+ l_mod_name: READABLE_STRING_8
+ do
+ if attached a_role as l_role then
+ create fs.make
+ fs.set_legend ("User Role")
+ create ti.make_with_text ("role", a_role.name)
+ ti.set_label ("Role")
+ ti.enable_required
+ fs.extend (ti)
+ a_form.extend (fs)
+
+ a_form.extend_html_text (" ")
+
+ create fs.make
+ fs.set_legend ("Permissions")
+
+ if
+ attached api.user_api.role_permissions as l_permissions_by_module
+ then
+ l_role_permissions := l_role.permissions
+ l_role_permissions.compare_objects
+
+ create l_module_names.make (l_permissions_by_module.count)
+ across
+ l_permissions_by_module as mod_ic
+ loop
+ l_module_names.force (mod_ic.key)
+ end
+ string_sorter.sort (l_module_names)
+ across
+ l_module_names as mod_ic
+ loop
+ l_mod_name := mod_ic.item
+ if
+ attached l_permissions_by_module.item (l_mod_name) as l_permissions and then
+ not l_permissions.is_empty
+ then
+ if l_mod_name.is_whitespace then
+ l_mod_name := "... "
+ end
+
+ create lab.make_with_text ("" + l_mod_name + " module ")
+
+ fs.extend (lab)
+ string_sorter.sort (l_permissions)
+ across l_permissions as ic loop
+ create cb.make_with_value ("cms_permissions", ic.item)
+ cb.set_checked (across l_role_permissions as rp_ic some rp_ic.item.is_case_insensitive_equal (ic.item) end)
+ cb.set_title (ic.item)
+ fs.extend (cb)
+ end
+ end
+ end
+ end
+ create ti.make ("new_cms_permissions[]")
+ fs.extend (ti)
+ fs.extend_html_text ("
")
+ fs.extend_html_text ("Add More Permissions ")
+
+
+ a_form.extend (fs)
+ add_javascript_content (script_add_remove_items)
+
+ create ts.make ("op")
+ ts.set_default_value ("Update role")
+ a_form.extend (ts)
+ a_form.extend_html_text (" ")
+
+ else
+ create fs.make
+ fs.set_legend ("User Role")
+ create ti.make ("role")
+ ti.set_label ("Role")
+ ti.enable_required
+ fs.extend (ti)
+ a_form.extend (fs)
+ a_form.extend_html_text (" ")
+ create ts.make ("op")
+ ts.set_default_value ("Create role")
+ a_form.extend (ts)
+ a_form.extend_html_text (" ")
+ end
+ end
+
+ update_role (a_form_data: WSF_FORM_DATA; a_role: CMS_USER_ROLE)
+ -- Update node `a_node' with form_data `a_form_data' for the given content type `a_content_type'.
+ local
+ l_perm: READABLE_STRING_8
+ do
+ if attached a_form_data.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Update role") then
+ if
+ attached a_form_data.string_item("role") as l_role_name and then
+ attached a_form_data.string_item ("role-id") as l_role_id
+ and then attached {CMS_USER_ROLE} api.user_api.user_role_by_id (l_role_id.to_integer) as l_role
+ then
+ if attached {WSF_STRING} a_form_data.item ("cms_permissions") as u_role then
+ a_role.permissions.wipe_out
+ a_role.add_permission (u_role.value)
+ elseif attached {WSF_MULTIPLE_STRING} a_form_data.item ("cms_permissions") as u_permissions then
+ a_role.permissions.wipe_out
+ -- Enable checked permissions.
+ across
+ u_permissions as ic
+ loop
+ l_perm := ic.item.value.as_string_8
+ if not l_perm.is_whitespace then
+ a_role.add_permission (l_perm)
+ end
+ end
+ else
+ a_role.permissions.wipe_out
+ end
+ if attached {WSF_TABLE} a_form_data.item ("new_cms_permissions[]") as l_cms_perms then
+ -- Add new permissions as checked.
+ across
+ l_cms_perms.values as ic
+ loop
+ if attached {WSF_STRING} ic.item as p then
+ l_perm := p.value.as_string_8
+ if not l_perm.is_whitespace then
+ a_role.add_permission (l_perm)
+ end
+ end
+ end
+ end
+
+ if not a_form_data.has_error then
+ a_role.set_name (l_role_name)
+ api.user_api.save_user_role (a_role)
+ if not api.user_api.has_error then
+ add_success_message ("Permissions updated")
+ set_redirection (absolute_url ("admin/role/" + a_role.id.out, Void))
+ else
+ add_error_message ("Error during permissions update operation.")
+ end
+ end
+ else
+ a_form_data.report_error ("Missing Role")
+ end
+ end
+ end
+ end
+
+ create_role (a_form_data: WSF_FORM_DATA)
+ local
+ u: CMS_USER_ROLE
+ do
+ if attached a_form_data.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Create role") then
+ if attached a_form_data.string_item ("role") as l_role then
+ create u.make (l_role)
+ api.user_api.save_user_role (u)
+ if api.user_api.has_error then
+ -- handle error
+ else
+ add_success_message ("Created Role " + link (l_role, "admin/role/" + u.id.out, Void))
+ set_redirection (absolute_url ("admin/role/" + u.id.out, Void))
+ end
+ else
+ a_form_data.report_invalid_field ("username", "Missing role!")
+ end
+ end
+ end
+ end
+
+feature -- Generation
+
+ script_add_remove_items: STRING = "[
+ $(document).ready(function() {
+ var wrapper = $(".input_fields_wrap"); //Fields wrapper
+ var add_button = $(".add_field_button"); //Add button ID
+
+ $(add_button).click(function(e){ //on add input button click
+ e.preventDefault();
+ $(wrapper).append(''); //add input box
+ });
+
+ $(wrapper).on("click",".remove_field", function(e){ //user click on remove text
+ e.preventDefault(); $(this).parent('div').remove(); x--;
+ })
+ });
+ ]"
+
+end
diff --git a/modules/admin/handler/role/cms_role_handler.e b/modules/admin/handler/role/cms_role_handler.e
new file mode 100644
index 0000000..0b9a852
--- /dev/null
+++ b/modules/admin/handler/role/cms_role_handler.e
@@ -0,0 +1,203 @@
+note
+ description: "[
+ Handler for a CMS user in the CMS interface
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ROLE_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post,
+ do_delete
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+feature -- Query
+
+ role_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- User id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_role: detachable CMS_USER_ROLE
+ l_uid: INTEGER_64
+ edit_response: CMS_ROLE_FORM_RESPONSE
+ view_response: CMS_ROLE_VIEW_RESPONSE
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("admin roles") then
+ if req.percent_encoded_path_info.ends_with_general ("/edit") then
+ check valid_url: req.percent_encoded_path_info.starts_with_general ("/admin/role/") end
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with_general ("/delete") then
+ check valid_url: req.percent_encoded_path_info.starts_with_general ("/admin/role/") end
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ else
+ -- Display existing node
+ l_uid := role_id_path_parameter (req)
+ if l_uid > 0 then
+ l_role := api.user_api.user_role_by_id (l_uid.to_integer)
+ if
+ l_role /= Void
+ then
+ create view_response.make (req, res, api)
+ view_response.execute
+ else
+ send_not_found (req, res)
+ end
+ else
+ create_new_role (req, res)
+ end
+ end
+ else
+ r.execute
+ end
+ end
+
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ edit_response: CMS_ROLE_FORM_RESPONSE
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("admin roles") then
+ if req.percent_encoded_path_info.ends_with_general ("/edit") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with_general ("/delete") then
+ if
+ attached {WSF_STRING} req.form_parameter ("op") as l_op and then
+ l_op.value.same_string ("Delete")
+ then
+ do_delete (req, res)
+ end
+ elseif req.percent_encoded_path_info.ends_with_general ("/add/role") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ end
+ else
+ r.execute
+ end
+ end
+
+feature -- Error
+
+ do_error (req: WSF_REQUEST; res: WSF_RESPONSE; a_id: detachable WSF_STRING)
+ -- Handling error.
+ local
+ l_page: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ l_page.set_value (req.absolute_script_url (req.percent_encoded_path_info), "request")
+ if a_id /= Void and then a_id.is_integer then
+ -- resource not found
+ l_page.set_value ("404", "code")
+ l_page.set_status_code (404)
+ else
+ -- bad request
+ l_page.set_value ("400", "code")
+ l_page.set_status_code (400)
+ end
+ l_page.execute
+ end
+
+ do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ do
+ if attached current_user (req) as l_user then
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached api.user_api.user_role_by_id (l_id.integer_value) as l_role
+ then
+ api.user_api.delete_role (l_role)
+ res.send (create {CMS_REDIRECTION_RESPONSE_MESSAGE}.make (req.absolute_script_url ("")))
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ else
+ send_access_denied (req, res)
+ end
+ end
+
+
+feature {NONE} -- New role
+
+ create_new_role (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ edit_response: CMS_ROLE_FORM_RESPONSE
+ do
+ if req.percent_encoded_path_info.starts_with_general ("/admin/add/role") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ else
+ send_bad_request (req, res)
+ end
+ end
+
+end
diff --git a/modules/admin/handler/role/cms_role_view_response.e b/modules/admin/handler/role/cms_role_view_response.e
new file mode 100644
index 0000000..a6887ae
--- /dev/null
+++ b/modules/admin/handler/role/cms_role_view_response.e
@@ -0,0 +1,93 @@
+note
+ description: "Summary description for {CMS_ROLE_VIEW_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ROLE_VIEW_RESPONSE
+
+inherit
+ CMS_RESPONSE
+
+create
+ make
+
+feature -- Query
+
+ role_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- Role id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+
+feature -- Execution
+
+ process
+ -- Computed response message.
+ local
+ uid: INTEGER_64
+ user_api : CMS_USER_API
+ do
+ user_api := api.user_api
+ uid := role_id_path_parameter (request)
+ if uid > 0 and then attached user_api.user_role_by_id (uid.to_integer) as l_role then
+ append_html_to_output (l_role, Current)
+ else
+ set_main_content ("Missing Role")
+ end
+ end
+
+
+ append_html_to_output (a_role: CMS_USER_ROLE; a_response: CMS_RESPONSE )
+ local
+ lnk: CMS_LOCAL_LINK
+ s: STRING
+ do
+ a_response.set_value (a_role, "role")
+ create lnk.make (a_response.translation ("View", Void), "admin/role/" + a_role.id.out)
+ lnk.set_is_active (True)
+ lnk.set_weight (1)
+ a_response.add_to_primary_tabs (lnk)
+ create lnk.make (a_response.translation ("Edit", Void), "admin/role/" + a_role.id.out + "/edit")
+ lnk.set_weight (2)
+ a_response.add_to_primary_tabs (lnk)
+
+ if a_role /= Void and then a_role.id > 0 then
+ create lnk.make (a_response.translation ("Delete", Void), "admin/role/" + a_role.id.out + "/delete")
+ lnk.set_weight (3)
+ a_response.add_to_primary_tabs (lnk)
+ end
+
+ create s.make_empty
+ s.append (" ")
+ s.append ("
Role Information ")
+ s.append ("
Role:")
+ s.append (a_role.name)
+ s.append ("
")
+
+ s.append ("
Permissions: ")
+ if
+ not a_role.permissions.is_empty
+ then
+ s.append ("
%N")
+ across a_role.permissions as ic loop
+ s.append (""+ ic.item + " %N")
+ end
+ s.append (" %N")
+
+ end
+
+ s.append ("
")
+ a_response.set_title (a_role.name)
+ a_response.set_main_content (s)
+ end
+
+end
diff --git a/modules/admin/handler/user/cms_admin_users_handler.e b/modules/admin/handler/user/cms_admin_users_handler.e
new file mode 100644
index 0000000..4a47833
--- /dev/null
+++ b/modules/admin/handler/user/cms_admin_users_handler.e
@@ -0,0 +1,128 @@
+note
+ description: "Summary description for {CMS_ADMIN_USER_HANDLER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_ADMIN_USERS_HANDLER
+
+inherit
+
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ u: CMS_USER
+ l_page_helper: CMS_PAGINATION_GENERATOR
+ s_pager: STRING
+ l_count: INTEGER
+ user_api: CMS_USER_API
+ do
+ -- At the moment the template are hardcoded, but we can
+ -- get them from the configuration file and load them into
+ -- the setup class.
+
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} l_response.make (req, res, api)
+ if l_response.has_permission ("admin users") then
+ user_api := api.user_api
+
+ l_count := user_api.users_count
+
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+
+ create s.make_empty
+ if l_count > 1 then
+ l_response.set_title ("Listing " + l_count.out + " Users")
+ else
+ l_response.set_title ("Listing " + l_count.out + " User")
+ end
+
+ create s_pager.make_empty
+ create l_page_helper.make ("admin/users/?page={page}&size={size}", user_api.users_count.as_natural_64, 25) -- FIXME: Make this default page size a global CMS settings
+ l_page_helper.get_setting_from_request (req)
+ if l_page_helper.has_upper_limit and then l_page_helper.pages_count > 1 then
+ l_page_helper.append_to_html (l_response, s_pager)
+ if l_page_helper.page_size > 25 then
+ s.append (s_pager)
+ end
+ end
+
+ if attached user_api.recent_users (create {CMS_DATA_QUERY_PARAMETERS}.make (l_page_helper.current_page_offset, l_page_helper.page_size)) as lst then
+ s.append ("%N")
+ end
+ -- Again the pager at the bottom, if needed
+ s.append (s_pager)
+
+ if l_response.has_permission ("manage " + {CMS_ADMIN_MODULE}.name) then
+ s.append (l_response.link ("Add User", "admin/add/user", Void))
+ end
+
+ l_response.set_main_content (s)
+ l_response.execute
+ else
+ l_response.execute
+ end
+ end
+end
diff --git a/modules/admin/handler/user/cms_user_form_response.e b/modules/admin/handler/user/cms_user_form_response.e
new file mode 100644
index 0000000..40847f0
--- /dev/null
+++ b/modules/admin/handler/user/cms_user_form_response.e
@@ -0,0 +1,502 @@
+note
+ description: "Summary description for {CMS_USER_FORM_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_USER_FORM_RESPONSE
+
+inherit
+ CMS_RESPONSE
+
+create
+ make
+
+feature -- Query
+
+ user_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- User id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- Process
+
+ process
+ -- Computed response message.
+ local
+ b: STRING_8
+ uid: INTEGER_64
+ user_api: CMS_USER_API
+ do
+ user_api := api.user_api
+ create b.make_empty
+ uid := user_id_path_parameter (request)
+ if
+ uid > 0 and then
+ attached user_api.user_by_id (uid) as l_user
+ then
+ if
+ location.ends_with_general ("/edit")
+ then
+ edit_form (l_user)
+ elseif location.ends_with_general ("/delete") then
+ delete_form (l_user)
+ end
+ else
+ new_form
+ end
+ end
+
+feature -- Process Edit
+
+ edit_form (a_user: CMS_USER)
+ local
+ f: like new_edit_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ do
+ create b.make_empty
+ f := new_edit_form (a_user, url (location, Void), "edit-user")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.submit_actions.extend (agent edit_form_submit (?, a_user, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_user.has_id then
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("View", Void),"admin/user/" + a_user.id.out), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void),"admin/user/" + a_user.id.out + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Delete", Void),"admin/user/" + a_user.id.out + "/delete"), primary_tabs)
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_user.name)
+ b.append (html_encoded (a_user.name) + " saved")
+ else
+ set_title (formatted_string (translation ("Edit $1 #$2", Void), [a_user.name, a_user.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+feature -- Process Delete
+
+ delete_form (a_user: CMS_USER)
+ local
+ f: like new_delete_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ do
+ create b.make_empty
+ f := new_delete_form (a_user, url (location, Void), "edit-user")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_user.has_id then
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("View", Void),"admin/user/" + a_user.id.out ), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void),"admin/user/" + a_user.id.out + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Delete", Void),"admin/user/" + a_user.id.out + "/delete"), primary_tabs)
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_user.name)
+ b.append (html_encoded (a_user.name) + " deleted")
+ else
+ set_title (formatted_string (translation ("Delete $1 #$2", Void), [a_user.name, a_user.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+
+feature -- Process New
+
+ new_form
+ local
+ f: like new_edit_form
+ b: STRING
+ fd: detachable WSF_FORM_DATA
+ l_user: detachable CMS_USER
+ do
+ create b.make_empty
+ f := new_edit_form (l_user, url (location, Void), "create-user")
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.validation_actions.extend (agent new_form_validate (?, b))
+ f.submit_actions.extend (agent edit_form_submit (?, l_user, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ if attached l_user then
+ set_title (l_user.name)
+ b.append (html_encoded (l_user.name) + " Saved")
+ end
+ else
+ if attached l_user then
+ set_title (formatted_string (translation ("Saved $1 #$2", Void), [l_user.name, l_user.id]))
+ end
+ f.append_to_html (wsf_theme, b)
+ end
+ set_main_content (b)
+ end
+
+feature -- Form
+
+ edit_form_submit (fd: WSF_FORM_DATA; a_user: detachable CMS_USER; b: STRING)
+ local
+ l_update_roles: BOOLEAN
+ l_update_user: BOOLEAN
+ l_save_user: BOOLEAN
+ l_user: detachable CMS_USER
+ s: STRING
+ lnk: CMS_LINK
+ do
+
+ l_update_roles := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Update user role")
+ if l_update_roles then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ if a_user /= Void then
+ l_user := a_user
+ if l_user.has_id then
+ create {CMS_LOCAL_LINK} lnk.make (translation ("View", Void),"admin/user/" + l_user.id.out )
+ change_user (fd, a_user)
+ s := "modified"
+ set_redirection (lnk.location)
+ end
+ end
+ end
+ l_update_user := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Update user")
+ if l_update_user then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ if a_user /= Void then
+ l_user := a_user
+ if l_user.has_id then
+ change_user (fd, a_user)
+ s := "modified"
+ end
+ end
+ end
+ l_save_user := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Create user")
+ if l_save_user then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ create_user (fd)
+ end
+
+ end
+
+ new_edit_form (a_user: detachable CMS_USER; a_url: READABLE_STRING_8; a_name: STRING): CMS_FORM
+ -- Create a web form named `a_name' for uSER `a_YSER' (if set), using form action url `a_url'.
+ local
+ f: CMS_FORM
+ th: WSF_FORM_HIDDEN_INPUT
+ do
+ create f.make (a_url, a_name)
+
+ create th.make ("user-id")
+ if a_user /= Void then
+ th.set_text_value (a_user.id.out)
+ else
+ th.set_text_value ("0")
+ end
+ f.extend (th)
+
+ populate_form (f, a_user)
+
+ Result := f
+ end
+
+ new_form_validate (fd: WSF_FORM_DATA; b: STRING)
+ do
+ if attached fd.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Create user") then
+ if attached fd.string_item ("username") as l_username then
+ if attached api.user_api.user_by_name (l_username) then
+ fd.report_invalid_field ("username", "Username already taken!")
+ end
+ else
+ fd.report_invalid_field ("username", "missing username")
+ end
+ if attached fd.string_item ("email") as l_email then
+ if attached api.user_api.user_by_email (l_email) then
+ fd.report_invalid_field ("email", "Email address already associated with an existing account!")
+ end
+ else
+ fd.report_invalid_field ("email", "missing email address")
+ end
+ elseif f_op.is_case_insensitive_equal_general ("Update user") then
+ if attached fd.string_item ("username") as l_username then
+ if api.user_api.user_by_name (l_username) = Void then
+ fd.report_invalid_field ("username", "Username does not exist!")
+ end
+ else
+ fd.report_invalid_field ("username", "missing username")
+ end
+ end
+ end
+ end
+
+ new_delete_form (a_user: detachable CMS_USER; a_url: READABLE_STRING_8; a_name: STRING;): CMS_FORM
+ -- Create a web form named `a_name' for node `a_user' (if set), using form action url `a_url'.
+ local
+ f: CMS_FORM
+ ts: WSF_FORM_SUBMIT_INPUT
+ do
+ create f.make (a_url, a_name)
+ f.extend_html_text (" ")
+ f.extend_html_text ("Are you sure you want to delete? ")
+
+ -- TODO check if we need to check for has_permissions!!
+ if
+ a_user /= Void and then
+ a_user.has_id
+ then
+ create ts.make ("op")
+ ts.set_default_value ("Delete")
+ fixme ("[
+ ts.set_default_value (translation ("Delete"))
+ ]")
+
+ f.extend (ts)
+ create ts.make ("op")
+ ts.set_default_value ("Cancel")
+ ts.set_formmethod ("GET")
+ ts.set_formaction ("/admin/user/" + a_user.id.out)
+ f.extend (ts)
+ end
+
+ Result := f
+ end
+
+
+
+ populate_form (a_form: WSF_FORM; a_user: detachable CMS_USER)
+ -- Fill the web form `a_form' with data from `a_node' if set,
+ -- and apply this to content type `a_content_type'.
+ local
+ ti: WSF_FORM_TEXT_INPUT
+ fe: WSF_FORM_EMAIL_INPUT
+ fs: WSF_FORM_FIELD_SET
+ cb: WSF_FORM_CHECKBOX_INPUT
+ ts: WSF_FORM_SUBMIT_INPUT
+ l_user_roles: detachable LIST [CMS_USER_ROLE]
+ do
+ if a_user /= Void then
+ create fs.make
+ fs.set_legend ("Basic User Account Information")
+ fs.extend_html_text ("User name
")
+ fs.extend_html_text (a_user.name)
+ if attached a_user.email as l_email then
+ create fe.make_with_text ("email", l_email)
+ else
+ create fe.make_with_text ("email", "")
+ end
+ fe.set_label ("Email")
+ fe.enable_required
+ fs.extend (fe)
+ a_form.extend (fs)
+ a_form.extend_html_text (" ")
+ create ts.make ("op")
+ ts.set_default_value ("Update user")
+ a_form.extend (ts)
+ a_form.extend_html_text (" ")
+
+
+ create fs.make
+ fs.set_legend ("User Roles")
+
+ l_user_roles := api.user_api.user_roles (a_user)
+ if l_user_roles.is_empty then
+ l_user_roles := Void
+ end
+
+ across api.user_api.effective_roles as ic loop
+ create cb.make_with_value ("cms_roles", ic.item.id.out)
+ cb.set_checked (l_user_roles /= Void and then across l_user_roles as r_ic some r_ic.item.same_user_role (ic.item) end)
+ cb.set_title (ic.item.name)
+ fs.extend (cb)
+ end
+
+ a_form.extend (fs)
+ create ts.make ("op")
+ ts.set_default_value ("Update user role")
+ a_form.extend (ts)
+ else
+ create fs.make
+ fs.set_legend ("Basic User Account Information")
+ create ti.make ("username")
+ ti.set_label ("Username")
+ ti.enable_required
+ fs.extend (ti)
+ create fe.make_with_text ("email", "")
+ fe.set_label ("Email")
+ fe.enable_required
+ fs.extend (fe)
+ a_form.extend (fs)
+ a_form.extend_html_text (" ")
+ create ts.make ("op")
+ ts.set_default_value ("Create user")
+ a_form.extend (ts)
+ a_form.extend_html_text (" ")
+ end
+ end
+
+ change_user (a_form_data: WSF_FORM_DATA; a_user: CMS_USER)
+ -- Update node `a_node' with form_data `a_form_data' for the given content type `a_content_type'.
+ local
+ l_uroles: LIST [CMS_USER_ROLE]
+ do
+ if attached a_form_data.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Update user role") then
+ if attached a_form_data.string_item ("user-id") as l_user_id and then
+ attached {CMS_USER} api.user_api.user_by_id (l_user_id.to_integer) as l_user
+ then
+ l_uroles := api.user_api.user_roles (l_user)
+ l_uroles.compare_objects
+ if attached {WSF_STRING} a_form_data.item ("cms_roles") as l_role then
+ if attached api.user_api.user_role_by_id (l_role.integer_value) as role then
+ if not l_uroles.has (role) then
+ api.user_api.assign_role_to_user (role, a_user)
+ end
+ end
+ elseif attached {WSF_MULTIPLE_STRING} a_form_data.item ("cms_roles") as l_roles then
+ across l_roles as ic loop
+ if attached api.user_api.user_role_by_id (ic.item.integer_value) as role then
+ if not l_uroles.has (role) then
+ api.user_api.assign_role_to_user (role, a_user)
+ end
+ end
+ end
+ else
+ across api.user_api.roles as ic loop
+ api.user_api.unassign_role_from_user (ic.item, a_user)
+ end
+ end
+ add_success_message ("Roles updated")
+ else
+ a_form_data.report_error ("Missing User")
+ end
+ elseif f_op.is_case_insensitive_equal_general ("Update user") then
+ if
+ attached a_form_data.string_item ("user-id") as l_user_id and then
+ attached {CMS_USER} api.user_api.user_by_id (l_user_id.to_integer) as l_user
+ then
+ if
+ attached a_form_data.string_item ("email") as l_email
+ then
+ if
+ attached l_user.email as u_email and then
+ not u_email.is_case_insensitive_equal_general (l_email) and then
+ api.user_api.user_by_email (l_email) = Void
+ then
+ -- Valid email
+ a_user.set_email (l_email)
+ else
+ if attached l_user.email as u_email and then not u_email.is_case_insensitive_equal_general (l_email) then
+ a_form_data.report_invalid_field ("email", "Email already exist!")
+ end
+ end
+ if not a_form_data.has_error then
+ api.user_api.update_user (a_user)
+ add_success_message ("Updated basic info")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ create_user (a_form_data: WSF_FORM_DATA)
+ local
+ u: CMS_USER
+ do
+ if attached a_form_data.string_item ("op") as f_op then
+ if f_op.is_case_insensitive_equal_general ("Create user") then
+ if
+ attached a_form_data.string_item ("username") as l_username and then
+ attached a_form_data.string_item ("email") as l_email and then
+ l_email.is_valid_as_string_8
+ then
+ create u.make (l_username)
+ u.set_email (l_email.as_string_8)
+ u.set_password (new_random_password (u))
+ api.user_api.new_user (u)
+ if api.user_api.has_error then
+ -- handle error
+ else
+ add_success_message ("Created user")
+ end
+
+ else
+ a_form_data.report_invalid_field ("username", "Missing username!")
+ a_form_data.report_invalid_field ("email", "Missing email address!")
+ end
+ end
+ end
+ end
+
+
+feature -- Generation
+
+ new_random_password (u: CMS_USER): STRING
+ -- Generate a new token activation token
+ local
+ l_token: STRING
+ l_security: SECURITY_PROVIDER
+ l_encode: URL_ENCODER
+ do
+ create l_security
+ l_token := l_security.token
+ create l_encode
+ from until l_token.same_string (l_encode.encoded_string (l_token)) loop
+ -- Loop ensure that we have a security token that does not contain characters that need encoding.
+ -- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token
+ -- but the user will need to use an unencoded token if activation has to be done manually.
+ l_token := l_security.token
+ end
+ Result := l_token + url_encoded (u.name) + u.creation_date.out
+ end
+
+
+end
diff --git a/modules/admin/handler/user/cms_user_handler.e b/modules/admin/handler/user/cms_user_handler.e
new file mode 100644
index 0000000..4bfd764
--- /dev/null
+++ b/modules/admin/handler/user/cms_user_handler.e
@@ -0,0 +1,203 @@
+note
+ description: "[
+ Handler for a CMS user in the CMS interface
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_USER_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post,
+ do_delete
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+feature -- Query
+
+ user_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- User id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_user: detachable CMS_USER
+ l_uid: INTEGER_64
+ edit_response: CMS_USER_FORM_RESPONSE
+ view_response: CMS_USER_VIEW_RESPONSE
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("admin users") then
+ if req.percent_encoded_path_info.ends_with_general ("/edit") then
+ check valid_url: req.percent_encoded_path_info.starts_with_general ("/admin/user/") end
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with_general ("/delete") then
+ check valid_url: req.percent_encoded_path_info.starts_with_general ("/admin/user/") end
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ else
+ -- Display existing node
+ l_uid := user_id_path_parameter (req)
+ if l_uid > 0 then
+ l_user := api.user_api.user_by_id (l_uid)
+ if
+ l_user /= Void
+ then
+ create view_response.make (req, res, api)
+ view_response.execute
+ else
+ send_not_found (req, res)
+ end
+ else
+ create_new_user (req, res)
+ end
+ end
+ else
+ r.execute
+ end
+ end
+
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ edit_response: CMS_USER_FORM_RESPONSE
+ r: CMS_RESPONSE
+ do
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("admin users") then
+ if req.percent_encoded_path_info.ends_with_general ("/edit") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with_general ("/delete") then
+ if
+ attached {WSF_STRING} req.form_parameter ("op") as l_op and then
+ l_op.value.same_string ("Delete")
+ then
+ do_delete (req, res)
+ end
+ elseif req.percent_encoded_path_info.ends_with_general ("/add/user") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ end
+ else
+ r.execute
+ end
+ end
+
+feature -- Error
+
+ do_error (req: WSF_REQUEST; res: WSF_RESPONSE; a_id: detachable WSF_STRING)
+ -- Handling error.
+ local
+ l_page: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ l_page.set_value (req.absolute_script_url (req.percent_encoded_path_info), "request")
+ if a_id /= Void and then a_id.is_integer then
+ -- resource not found
+ l_page.set_value ("404", "code")
+ l_page.set_status_code (404)
+ else
+ -- bad request
+ l_page.set_value ("400", "code")
+ l_page.set_status_code (400)
+ end
+ l_page.execute
+ end
+
+ do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ do
+ if attached current_user (req) as l_user then
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached api.user_api.user_by_id (l_id.integer_value) as u_user
+ then
+ api.user_api.delete_user(u_user)
+ res.send (create {CMS_REDIRECTION_RESPONSE_MESSAGE}.make (req.absolute_script_url ("")))
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ else
+ send_access_denied (req, res)
+ end
+ end
+
+
+feature {NONE} -- New User
+
+ create_new_user (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ edit_response: CMS_USER_FORM_RESPONSE
+ do
+ if req.percent_encoded_path_info.starts_with ("/admin/add/user") then
+ create edit_response.make (req, res, api)
+ edit_response.execute
+ else
+ send_bad_request (req, res)
+ end
+ end
+
+end
diff --git a/modules/admin/handler/user/cms_user_view_response.e b/modules/admin/handler/user/cms_user_view_response.e
new file mode 100644
index 0000000..4cf3716
--- /dev/null
+++ b/modules/admin/handler/user/cms_user_view_response.e
@@ -0,0 +1,110 @@
+note
+ description: "Summary description for {CMS_USER_VIEW_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_USER_VIEW_RESPONSE
+
+inherit
+ CMS_RESPONSE
+
+create
+ make
+
+feature -- Query
+
+ user_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- User id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+
+feature -- Execution
+
+ process
+ -- Computed response message.
+ local
+ uid: INTEGER_64
+ user_api : CMS_USER_API
+ do
+ user_api := api.user_api
+ uid := user_id_path_parameter (request)
+ if uid > 0 and then attached user_api.user_by_id (uid) as l_user then
+ append_html_to_output (l_user, Current)
+ else
+ set_main_content ("Missing User")
+ end
+ end
+
+ append_html_to_output (a_user: CMS_USER; a_response: CMS_RESPONSE)
+ local
+ lnk: CMS_LOCAL_LINK
+ s: STRING
+ l_role: CMS_USER_ROLE
+ do
+ a_response.set_value (a_user, "user")
+ create lnk.make (a_response.translation ("View", Void), "admin/user/" + a_user.id.out)
+ lnk.set_is_active (True)
+ lnk.set_weight (1)
+ a_response.add_to_primary_tabs (lnk)
+ create lnk.make (a_response.translation ("Edit", Void), "admin/user/" + a_user.id.out + "/edit")
+ lnk.set_permission_arguments (<<"manage admin", "manage users", "manage own user">>)
+ lnk.set_weight (2)
+ a_response.add_to_primary_tabs (lnk)
+
+ if a_user /= Void and then a_user.id > 0 then
+ create lnk.make (a_response.translation ("Delete", Void), "admin/user/" + a_user.id.out + "/delete")
+ lnk.set_weight (3)
+ a_response.add_to_primary_tabs (lnk)
+ end
+
+ -- FIXME: [04/aug/2015] use a CMS_FORM rather than hardcoded html.
+ -- So that other module may easily integrate them-selves to add information.
+ create s.make_empty
+ s.append (" ")
+ s.append ("
Account Information ")
+ s.append ("
Username: ")
+ s.append (a_user.name)
+ s.append ("
")
+ if attached a_user.email as l_email then
+ s.append ("
Email: ")
+ s.append (l_email)
+ s.append ("
")
+ end
+
+ if
+ attached {LIST [CMS_USER_ROLE]} api.user_api.user_roles (a_user) as l_roles and then
+ not l_roles.is_empty
+ then
+ s.append ("
Role(s): ")
+ across l_roles as ic loop
+ l_role := ic.item
+ s.append ("
")
+ s.append (link (l_role.name, "admin/role/" + l_role.id.out, Void))
+ s.append (" ")
+ debug
+ s.append ("
Permissions: ")
+ s.append ("
%N")
+ across l_role.permissions as perms_ic loop
+ s.append ("" + perms_ic.item + " %N")
+ end
+ s.append (" %N")
+ end
+ end
+ end
+
+ s.append ("
")
+ a_response.set_title (a_user.name)
+ a_response.set_main_content (s)
+ end
+
+end
diff --git a/modules/admin/site/files/css/admin.css b/modules/admin/site/files/css/admin.css
new file mode 100644
index 0000000..46f06ca
--- /dev/null
+++ b/modules/admin/site/files/css/admin.css
@@ -0,0 +1,34 @@
+ul.cms-users {
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc; }
+ ul.cms-users li {
+ border-top: dotted 1px #ccc; }
+ ul.cms-users li:first-child {
+ border-top: none; }
+ ul.cms-users li.cms_user a::before {
+ content: "[users] "; }
+
+ul.cms-roles {
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc; }
+ ul.cms-roles li {
+ border-top: dotted 1px #ccc; }
+ ul.cms-roles li:first-child {
+ border-top: none; }
+ ul.cms-roles li.cms_role a::before {
+ content: "[roles] "; }
+
+ul.cms-permissions {
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc; }
+ ul.cms-permissions li {
+ border-top: dotted 1px #ccc; }
+ ul.cms-permissions li:first-child {
+ border-top: none; }
+ ul.cms-permissions li.cms_permission a::before {
+ content: "[permission] "; }
+
+/*# sourceMappingURL=admin.css.map */
diff --git a/modules/admin/site/files/scss/admin.scss b/modules/admin/site/files/scss/admin.scss
new file mode 100644
index 0000000..3068a8e
--- /dev/null
+++ b/modules/admin/site/files/scss/admin.scss
@@ -0,0 +1,59 @@
+ul.cms-users {
+
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc;
+
+ li{
+ border-top: dotted 1px #ccc;
+ &:first-child {
+ border-top: none;
+ }
+ }
+
+ li.cms_user a::before {
+ content: "[users] ";
+ }
+}
+
+
+ul.cms-roles {
+
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc;
+
+ li{
+ border-top: dotted 1px #ccc;
+ &:first-child {
+ border-top: none;
+ }
+ }
+
+ li.cms_role a::before {
+ content: "[roles] ";
+ }
+}
+
+
+ul.cms-permissions {
+
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc;
+
+ li{
+ border-top: dotted 1px #ccc;
+ &:first-child {
+ border-top: none;
+ }
+ }
+
+ li.cms_permission a::before {
+ content: "[permission] ";
+ }
+}
+
+
+
+
diff --git a/modules/auth/auth-safe.ecf b/modules/auth/auth-safe.ecf
new file mode 100644
index 0000000..5824089
--- /dev/null
+++ b/modules/auth/auth-safe.ecf
@@ -0,0 +1,34 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/auth/cms_authentication_email_service_parameters.e b/modules/auth/cms_authentication_email_service_parameters.e
new file mode 100644
index 0000000..efb7b2a
--- /dev/null
+++ b/modules/auth/cms_authentication_email_service_parameters.e
@@ -0,0 +1,263 @@
+note
+ description: "Summary description for {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS
+
+inherit
+ EMAIL_SERVICE_PARAMETERS
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_cms_api: CMS_API)
+ local
+ utf: UTF_CONVERTER
+ l_site_name: READABLE_STRING_8
+ s: detachable READABLE_STRING_32
+ l_contact_email, l_subject_register, l_subject_activate, l_subject_password, l_subject_oauth: detachable READABLE_STRING_8
+ do
+ cms_api := a_cms_api
+ -- Use global smtp setting if any, otherwise "localhost"
+ smtp_server := utf.escaped_utf_32_string_to_utf_8_string_8 (a_cms_api.setup.text_item_or_default ("smtp", "localhost"))
+ l_site_name := utf.escaped_utf_32_string_to_utf_8_string_8 (a_cms_api.setup.site_name)
+ admin_email := a_cms_api.setup.site_email
+
+ if not admin_email.has ('<') then
+ admin_email := l_site_name + " <" + admin_email +">"
+ end
+
+ if attached {CONFIG_READER} a_cms_api.module_configuration_by_name ({CMS_AUTHENTICATION_MODULE}.name, Void) as cfg then
+ if attached cfg.text_item ("smtp") as l_smtp then
+ -- Overwrite global smtp setting if any.
+ smtp_server := utf.utf_32_string_to_utf_8_string_8 (l_smtp)
+ end
+ s := cfg.text_item ("email")
+ if s /= Void then
+ l_contact_email := utf.utf_32_string_to_utf_8_string_8 (s)
+ end
+ s := cfg.text_item ("subject_register")
+ if s /= Void then
+ l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s)
+ end
+ s := cfg.text_item ("subject_activate")
+ if s /= Void then
+ l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s)
+ end
+ s := cfg.text_item ("subject_password")
+ if s /= Void then
+ l_subject_register := utf.utf_32_string_to_utf_8_string_8 (s)
+ end
+ s := cfg.text_item ("subject_oauth")
+ if s /= Void then
+ l_subject_oauth := utf.utf_32_string_to_utf_8_string_8 (s)
+ end
+
+ end
+ if l_contact_email /= Void then
+ if not l_contact_email.has ('<') then
+ l_contact_email := l_site_name + " <" + l_contact_email + ">"
+ end
+ contact_email := l_contact_email
+ else
+ contact_email := admin_email
+ end
+ if l_subject_register /= Void then
+ contact_subject_register := l_subject_register
+ else
+ contact_subject_register := "Thank you for registering with us."
+ end
+
+ if l_subject_activate /= Void then
+ contact_subject_activate := l_subject_activate
+ else
+ contact_subject_activate := "New account activation token."
+ end
+ if l_subject_password /= Void then
+ contact_subject_password := l_subject_password
+ else
+ contact_subject_password := "Password Recovery."
+ end
+ if l_subject_oauth /= Void then
+ contact_subject_oauth := l_subject_oauth
+ else
+ contact_subject_oauth := "Welcome."
+ end
+
+ end
+
+feature -- Access
+
+ cms_api: CMS_API
+
+ smtp_server: IMMUTABLE_STRING_8
+
+ admin_email: IMMUTABLE_STRING_8
+
+ contact_email: IMMUTABLE_STRING_8
+ -- Contact email.
+
+ contact_subject_register: IMMUTABLE_STRING_8
+ contact_subject_activate: IMMUTABLE_STRING_8
+ contact_subject_password: IMMUTABLE_STRING_8
+ contact_subject_oauth: IMMUTABLE_STRING_8
+
+ account_activation: STRING
+ -- Account activation template email message.
+ do
+ Result := template_string ("account_activation.html", default_template_account_activation)
+ end
+
+ account_re_activation: STRING
+ -- Account re_activation template email message.
+ do
+ Result := template_string ("accunt_re_activation.html", default_template_account_re_activation)
+ end
+
+ account_password: STRING
+ -- Account password template email message.
+ do
+ Result := template_string ("account_new_password.html", default_template_account_new_password)
+ end
+
+ account_welcome: STRING
+ -- Account welcome template email message.
+ do
+ Result := template_string ("account_welcome.html", default_template_account_welcome)
+ end
+
+feature {NONE} -- Implementation: Template
+
+ template_path (a_name: READABLE_STRING_GENERAL): PATH
+ -- Location of template named `a_name'.
+ local
+ p: PATH
+ do
+ create p.make_from_string (a_name)
+ Result := cms_api.module_location_by_name ({CMS_AUTHENTICATION_MODULE}.name).extended ("mail_templates").extended (a_name)
+ end
+
+ template_string (a_name: READABLE_STRING_GENERAL; a_default: STRING): STRING
+ -- Content of template named `a_name', or `a_default' if template is not found.
+ local
+ p: PATH
+ do
+ p := template_path ("account_activation.html")
+ if attached read_template_file (p) as l_content then
+ Result := l_content
+ else
+ create Result.make_from_string (a_default)
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ read_template_file (a_path: PATH): detachable STRING
+ -- Read the content of the file at path `a_path'.
+ local
+ l_file: FILE
+ n: INTEGER
+ do
+ create {PLAIN_TEXT_FILE} l_file.make_with_path (a_path)
+ if l_file.exists and then l_file.is_readable then
+ n := l_file.count
+ l_file.open_read
+ l_file.read_stream (n)
+ Result := l_file.last_string
+ l_file.close
+ else
+ -- Error
+ end
+ end
+
+
+feature {NONE} -- Message email
+
+ default_template_account_activation: STRING = "[
+
+
+
+
+ Activation
+
+
+
+
+
+ Thank you for registering at ROC CMS
+
+ To complete your registration, please click on the following link to activate your account:
+
+
$link
+ Thank you for joining us.
+
+
+ ]"
+
+
+ default_template_account_re_activation: STRING = "[
+
+
+
+
+ New Activation
+
+
+
+
+
+ You have requested a new activation token at ROC CMS
+
+ To complete your registration, please click on the following link to activate your account:
+
+
$link
+ Thank you for joining us.
+
+
+ ]"
+
+
+
+ default_template_account_new_password: STRING = "[
+
+
+
+
+ New Password
+
+
+
+
+
+ You have required a new password at ROC CMS
+
+ To complete your request, please click on this link to generate a new password:
+
+
$link
+
+
+ ]"
+
+
+ default_template_account_welcome: STRING = "[
+
+
+
+
+ Welcome
+
+
+
+
+
+ Welcome toROC CMS
+ Thank you for joining us.
+
+
+ ]"
+
+end
diff --git a/modules/auth/cms_authentication_module.e b/modules/auth/cms_authentication_module.e
new file mode 100644
index 0000000..3787b75
--- /dev/null
+++ b/modules/auth/cms_authentication_module.e
@@ -0,0 +1,706 @@
+note
+ description: "Module Auth"
+ date: "$Date: 2015-05-20 06:50:50 -0300 (mi. 20 de may. de 2015) $"
+ revision: "$Revision: 97328 $"
+
+class
+ CMS_AUTHENTICATION_MODULE
+
+inherit
+ CMS_MODULE
+ redefine
+ setup_hooks,
+ permissions
+ end
+
+ CMS_HOOK_AUTO_REGISTER
+
+ CMS_HOOK_VALUE_TABLE_ALTER
+
+ CMS_HOOK_BLOCK
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ SHARED_EXECUTION_ENVIRONMENT
+ export
+ {NONE} all
+ end
+
+ REFACTORING_HELPER
+
+ SHARED_LOGGER
+
+ CMS_REQUEST_UTIL
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create current module
+ do
+ version := "1.0"
+ description := "Authentication module"
+ package := "authentication"
+
+ create root_dir.make_current
+ cache_duration := 0
+ end
+
+feature -- Access
+
+ name: STRING = "auth"
+
+ permissions: LIST [READABLE_STRING_8]
+ -- List of permission ids, used by this module, and declared.
+ do
+ Result := Precursor
+ Result.force ("account register")
+ end
+
+feature -- Access: docs
+
+ root_dir: PATH
+
+ cache_duration: INTEGER
+ -- Caching duration
+ --| 0: disable
+ --| -1: cache always valie
+ --| nb: cache expires after nb seconds.
+
+ cache_disabled: BOOLEAN
+ do
+ Result := cache_duration = 0
+ end
+
+feature -- Router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ configure_web (a_api, a_router)
+ end
+
+ configure_web (a_api: CMS_API; a_router: WSF_ROUTER)
+ do
+ a_router.handle ("/account", create {WSF_URI_AGENT_HANDLER}.make (agent handle_account (a_api, ?, ?)), a_router.methods_head_get)
+ a_router.handle ("/account/roc-login", create {WSF_URI_AGENT_HANDLER}.make (agent handle_login (a_api, ?, ?)), a_router.methods_head_get)
+ a_router.handle ("/account/roc-logout", create {WSF_URI_AGENT_HANDLER}.make (agent handle_logout (a_api, ?, ?)), a_router.methods_head_get)
+ a_router.handle ("/account/roc-register", create {WSF_URI_AGENT_HANDLER}.make (agent handle_register (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/activate/{token}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_activation (a_api, ?, ?)), a_router.methods_head_get)
+ a_router.handle ("/account/reactivate", create {WSF_URI_AGENT_HANDLER}.make (agent handle_reactivation (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/new-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_new_password (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/reset-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_reset_password (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/change-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_change_password (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/post-change-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_post_change_password (a_api, ?, ?)), a_router.methods_get)
+ end
+
+feature -- Hooks configuration
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ -- Module hooks configuration.
+ do
+ auto_subscribe_to_hooks (a_hooks)
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_value_table_alter_hook (Current)
+ end
+
+ value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE)
+ --
+ do
+ a_value.force (a_response.user, "user")
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ -- Hook execution on collection of menu contained by `a_menu_system'
+ -- for related response `a_response'.
+ local
+ lnk: CMS_LOCAL_LINK
+ do
+ if attached a_response.user as u then
+ create lnk.make (u.name, "account" )
+ lnk.set_weight (97)
+ a_menu_system.primary_menu.extend (lnk)
+ create lnk.make ("Logout", "account/roc-logout")
+ lnk.set_weight (98)
+ a_menu_system.primary_menu.extend (lnk)
+ else
+ create lnk.make ("Login", "account/roc-login")
+ lnk.set_weight (98)
+ a_menu_system.primary_menu.extend (lnk)
+ end
+
+ end
+
+feature -- Handler
+
+ handle_account (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+
+ if attached template_block ("account_info", r) as l_tpl_block then
+ if attached r.user as l_user then
+ r.set_value (api.user_api.user_roles (l_user), "roles")
+ end
+ r.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ r.add_warning_message ("Error with block [resources_page]")
+ end
+ end
+ r.execute
+ end
+
+ handle_login (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ if attached api.module_by_name ("basic_auth") then
+ -- FIXME: find better solution to support a default login system.
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_redirection (r.absolute_url ("/account/roc-basic-auth", Void))
+ r.execute
+ else
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.execute
+ end
+ end
+
+ handle_logout (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_redirection (r.absolute_url ("", Void))
+ r.execute
+ end
+
+ handle_register (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_user_api: CMS_USER_API
+ u: CMS_USER
+ l_exist: BOOLEAN
+ es: CMS_AUTHENTICATON_EMAIL_SERVICE
+ l_url: STRING
+ l_token: STRING
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if r.has_permission ("account register") then
+ if req.is_post_request_method then
+ if
+ attached {WSF_STRING} req.form_parameter ("name") as l_name and then
+ attached {WSF_STRING} req.form_parameter ("password") as l_password and then
+ attached {WSF_STRING} req.form_parameter ("email") as l_email
+ then
+ l_user_api := api.user_api
+
+ if attached l_user_api.user_by_name (l_name.value) then
+ -- Username already exist.
+ r.set_value ("User name already exists!", "error_name")
+ l_exist := True
+ end
+ if attached l_user_api.user_by_email (l_email.value) then
+ -- Emails already exist.
+ r.set_value ("An account is already associated with that email address!", "error_email")
+ l_exist := True
+ end
+
+ if not l_exist then
+ -- New user
+ create u.make (l_name.value)
+ u.set_email (l_email.value)
+ u.set_password (l_password.value)
+ l_user_api.new_user (u)
+
+ -- Create activation token
+ l_token := new_token
+ l_user_api.new_activation (l_token, u.id)
+ l_url := req.absolute_script_url ("/account/activate/" + l_token)
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle register: send_contact_email")
+ es.send_contact_email (l_email.value, l_url)
+
+ else
+ r.set_value (l_name.value, "name")
+ r.set_value (l_email.value, "email")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ end
+ end
+ else
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("You can also contact the webmaster to ask for an account.")
+ end
+
+ r.execute
+ end
+
+ handle_activation (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_user_api: CMS_USER_API
+ l_ir: INTERNAL_SERVER_ERROR_CMS_RESPONSE
+ do
+ l_user_api := api.user_api
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if attached {WSF_STRING} req.path_parameter ("token") as l_token then
+
+ if attached {CMS_USER} l_user_api.user_by_activation_token (l_token.value) as l_user then
+ -- Valid user_id
+ l_user.mark_active
+ l_user_api.update_user (l_user)
+ l_user_api.remove_activation (l_token.value)
+ r.set_main_content (" Your account "+ l_user.name +" has been activated
")
+ else
+ -- the token does not exist, or it was already used.
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ r.set_main_content ("The token " + l_token.value +" is not valid " + r.link ("Reactivate Account", "account/reactivate", Void) + "
")
+ end
+ r.execute
+ else
+ create l_ir.make (req, res, api)
+ l_ir.execute
+ end
+ end
+
+
+ handle_reactivation (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ es: CMS_AUTHENTICATON_EMAIL_SERVICE
+ l_user_api: CMS_USER_API
+ l_token: STRING
+ l_url: STRING
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if req.is_post_request_method then
+ if
+ attached {WSF_STRING} req.form_parameter ("email") as l_email
+ then
+ l_user_api := api.user_api
+ if attached {CMS_USER} l_user_api.user_by_email (l_email.value) as l_user then
+ -- User exist create a new token and send a new email.
+ if l_user.is_active then
+ r.set_value ("The asociated user to the given email " + l_email.value + " , is already active", "is_active")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ else
+ l_token := new_token
+ l_user_api.new_activation (l_token, l_user.id)
+ l_url := req.absolute_script_url ("/account/activate/" + l_token)
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle register: send_contact_activation_email")
+ es.send_contact_activation_email (l_email.value, l_url)
+ end
+ else
+ r.set_value ("The email does not exist or !", "error_email")
+ r.set_value (l_email.value, "email")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ end
+ end
+
+ r.execute
+ end
+
+ handle_new_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ es: CMS_AUTHENTICATON_EMAIL_SERVICE
+ l_user_api: CMS_USER_API
+ l_token: STRING
+ l_url: STRING
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if req.is_post_request_method then
+ l_user_api := api.user_api
+ if attached {WSF_STRING} req.form_parameter ("email") as l_email then
+ if attached {CMS_USER} l_user_api.user_by_email (l_email.value) as l_user then
+ -- User exist create a new token and send a new email.
+ l_token := new_token
+ l_user_api.new_password (l_token, l_user.id)
+ l_url := req.absolute_script_url ("/account/reset-password?token=" + l_token)
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle register: send_contact_password_email")
+ es.send_contact_password_email (l_email.value, l_url)
+ else
+ r.set_value ("The email does not exist !", "error_email")
+ r.set_value (l_email.value, "email")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ elseif attached {WSF_STRING} req.form_parameter ("username") as l_username then
+ if attached {CMS_USER} l_user_api.user_by_name (l_username) as l_user and then
+ attached l_user.email as l_email
+ then
+ -- User exist create a new token and send a new email.
+ l_token := new_token
+ l_user_api.new_password (l_token, l_user.id)
+ l_url := req.absolute_script_url ("/account/reset-password?token=" + l_token)
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle register: send_contact_password_email")
+ es.send_contact_password_email (l_email, l_url)
+ else
+ r.set_value ("The username does not exist !", "error_username")
+ r.set_value (l_username.value, "username")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ end
+ end
+ r.execute
+ end
+
+
+ handle_reset_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_user_api: CMS_USER_API
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ l_user_api := api.user_api
+ if attached {WSF_STRING} req.query_parameter ("token") as l_token then
+ r.set_value (l_token.value, "token")
+ if l_user_api.user_by_password_token (l_token.value) = Void then
+ r.set_value ("The token " + l_token.value + " is not valid, " + r.link ("click here" , "account/new-password", Void) + " to generate a new token.", "error_token")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ end
+
+ if req.is_post_request_method then
+
+ if
+ attached {WSF_STRING} req.form_parameter ("token") as l_token and then
+ attached {WSF_STRING} req.form_parameter ("password") as l_password and then
+ attached {WSF_STRING} req.form_parameter ("confirm_password") as l_confirm_password
+ then
+ -- Does the passwords match?
+ if l_password.value.same_string (l_confirm_password.value) then
+ -- is the token valid?
+ if attached {CMS_USER} l_user_api.user_by_password_token (l_token.value) as l_user then
+ l_user.set_password (l_password.value)
+ l_user_api.update_user (l_user)
+ l_user_api.remove_password (l_token.value)
+ end
+ else
+ r.set_value ("Passwords Don't Match", "error_password")
+ r.set_value (l_token.value, "token")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ end
+ end
+ end
+ r.execute
+ end
+
+ handle_change_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_user_api: CMS_USER_API
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ l_user_api := api.user_api
+
+ if req.is_post_request_method then
+ if attached r.user as l_user then
+ r.set_value (api.user_api.user_roles (l_user), "roles")
+ if
+ attached {WSF_STRING} req.form_parameter ("password") as l_password and then
+ attached {WSF_STRING} req.form_parameter ("confirm_password") as l_confirm_password and then
+ l_password.value.same_string (l_confirm_password.value)
+ then
+ -- Does the passwords match?
+ l_user.set_password (l_password.value)
+ l_user_api.update_user (l_user)
+ r.set_redirection (req.absolute_script_url ("/account/post-change-password"))
+ else
+ if attached template_block ("account_info", r) as l_tpl_block then
+-- r.set_value (l_user, "user")
+ r.set_value ("Passwords Don't Match", "error_password")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ r.add_block (l_tpl_block, "content")
+ end
+ end
+ end
+ end
+ r.execute
+ end
+
+ handle_post_change_password (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if attached template_block ("post_change", r) as l_tpl_block then
+ r.add_block (l_tpl_block, "content")
+ end
+ r.execute
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ local
+ l_string: STRING
+ do
+ Result := <<"register", "reactivate", "new_password", "reset_password">>
+ debug ("roc")
+ create l_string.make_empty
+ across
+ Result as ic
+ loop
+ l_string.append (ic.item)
+ l_string.append_character (' ')
+ end
+ write_debug_log (generator + ".block_list:" + l_string )
+ end
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if
+ a_block_id.is_case_insensitive_equal_general ("register") and then
+ a_response.location.starts_with ("account/roc-register")
+ then
+ get_block_view_register (a_block_id, a_response)
+ elseif
+ a_block_id.is_case_insensitive_equal_general ("reactivate") and then
+ a_response.location.starts_with ("account/reactivate")
+ then
+ get_block_view_reactivate (a_block_id, a_response)
+ elseif
+ a_block_id.is_case_insensitive_equal_general ("new_password") and then
+ a_response.location.starts_with ("account/new-password")
+ then
+ get_block_view_new_password (a_block_id, a_response)
+ elseif
+ a_block_id.is_case_insensitive_equal_general ("reset_password") and then
+ a_response.location.starts_with ("account/reset-password")
+ then
+ get_block_view_reset_password (a_block_id, a_response)
+ end
+ end
+
+feature {NONE} -- Token Generation
+
+ new_token: STRING
+ -- Generate a new token activation token
+ local
+ l_token: STRING
+ l_security: SECURITY_PROVIDER
+ l_encode: URL_ENCODER
+ do
+ create l_security
+ l_token := l_security.token
+ create l_encode
+ from until l_token.same_string (l_encode.encoded_string (l_token)) loop
+ -- Loop ensure that we have a security token that does not contain characters that need encoding.
+ -- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token
+ -- but the user will need to use an unencoded token if activation has to be done manually.
+ l_token := l_security.token
+ end
+ Result := l_token
+ end
+
+feature {NONE} -- Helpers
+
+ template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK
+ -- Smarty content block for `a_block_id'
+ local
+ p: detachable PATH
+ do
+ create p.make_from_string ("templates")
+ p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl")
+
+ p := a_response.api.module_theme_resource_location (Current, p)
+ if p /= Void then
+ if attached p.entry as e then
+ create Result.make (a_block_id, Void, p.parent, e)
+ else
+ create Result.make (a_block_id, Void, p.parent, p)
+ end
+ end
+ end
+
+feature {NONE} -- Block views
+
+-- get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+-- local
+---- vals: CMS_VALUE_TABLE
+-- do
+-- if attached template_block (a_block_id, a_response) as l_tpl_block then
+---- create vals.make (1)
+---- -- add the variable to the block
+---- value_table_alter (vals, a_response)
+---- across
+---- vals as ic
+---- loop
+---- l_tpl_block.set_value (ic.item, ic.key)
+---- end
+-- a_response.put_required_block (l_tpl_block, "content")
+-- else
+-- debug ("cms")
+-- a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+-- end
+-- end
+-- end
+
+ get_block_view_register (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if a_response.has_permission ("account register") then
+ if a_response.request.is_get_request_method then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ elseif a_response.request.is_post_request_method then
+ if a_response.values.has ("error_name") or else a_response.values.has ("error_email") then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ -- l_tpl_block.set_value (a_response.values.item ("error_name"), "error_name")
+ -- l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email")
+ -- l_tpl_block.set_value (a_response.values.item ("email"), "email")
+ -- l_tpl_block.set_value (a_response.values.item ("name"), "name")
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ else
+ if attached template_block ("post_register", a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ get_block_view_reactivate (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if a_response.request.is_get_request_method then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ elseif a_response.request.is_post_request_method then
+ if a_response.values.has ("error_email") or else a_response.values.has ("is_active") then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+-- l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email")
+-- l_tpl_block.set_value (a_response.values.item ("email"), "email")
+-- l_tpl_block.set_value (a_response.values.item ("is_active"), "is_active")
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ else
+ if attached template_block ("post_reactivate", a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+ end
+ end
+
+ get_block_view_new_password (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if a_response.request.is_get_request_method then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ elseif a_response.request.is_post_request_method then
+ if a_response.values.has ("error_email") or else a_response.values.has ("error_username") then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+-- l_tpl_block.set_value (a_response.values.item ("error_email"), "error_email")
+-- l_tpl_block.set_value (a_response.values.item ("email"), "email")
+-- l_tpl_block.set_value (a_response.values.item ("error_username"), "error_username")
+-- l_tpl_block.set_value (a_response.values.item ("username"), "username")
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ else
+ if attached template_block ("post_password", a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+ end
+ end
+
+ get_block_view_reset_password (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if a_response.request.is_get_request_method then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+-- l_tpl_block.set_value (a_response.values.item ("token"), "token")
+-- l_tpl_block.set_value (a_response.values.item ("error_token"), "error_token")
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ elseif a_response.request.is_post_request_method then
+ if a_response.values.has ("error_token") or else a_response.values.has ("error_password") then
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+-- l_tpl_block.set_value (a_response.values.item ("error_token"), "error_token")
+-- l_tpl_block.set_value (a_response.values.item ("error_password"), "error_password")
+-- l_tpl_block.set_value (a_response.values.item ("token"), "token")
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ else
+ if attached template_block ("post_reset", a_response) as l_tpl_block then
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+ end
+ end
+
+note
+ copyright: "Copyright (c) 1984-2013, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/modules/auth/cms_authenticaton_email_service.e b/modules/auth/cms_authenticaton_email_service.e
new file mode 100644
index 0000000..d4a1984
--- /dev/null
+++ b/modules/auth/cms_authenticaton_email_service.e
@@ -0,0 +1,88 @@
+note
+ description: "Summary description for {CMS_AUTHENTICATON_EMAIL_SERVICE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_AUTHENTICATON_EMAIL_SERVICE
+
+inherit
+ EMAIL_SERVICE
+ redefine
+ initialize,
+ parameters
+ end
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ initialize
+ do
+ Precursor
+ contact_email := parameters.contact_email
+ end
+
+ parameters: CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS
+ -- Associated parameters.
+
+feature -- Access
+
+ contact_email: IMMUTABLE_STRING_8
+ -- contact email.
+
+feature -- Basic Operations
+
+ send_contact_email (a_to, a_content: READABLE_STRING_8)
+ -- Send successful contact message `a_token' to `a_to'.
+ require
+ attached_to: a_to /= Void
+ local
+ l_message: STRING
+ do
+ create l_message.make_from_string (parameters.account_activation)
+ l_message.replace_substring_all ("$link", a_content)
+ send_message (contact_email, a_to, parameters.contact_subject_register, l_message)
+ end
+
+
+ send_contact_activation_email (a_to, a_content: READABLE_STRING_8)
+ -- Send successful contact message `a_token' to `a_to'.
+ require
+ attached_to: a_to /= Void
+ local
+ l_message: STRING
+ do
+ create l_message.make_from_string (parameters.account_re_activation)
+ l_message.replace_substring_all ("$link", a_content)
+ send_message (contact_email, a_to, parameters.contact_subject_activate, l_message)
+ end
+
+
+ send_contact_password_email (a_to, a_content: READABLE_STRING_8)
+ -- Send successful contact message `a_token' to `a_to'.
+ require
+ attached_to: a_to /= Void
+ local
+ l_message: STRING
+ do
+ create l_message.make_from_string (parameters.account_password)
+ l_message.replace_substring_all ("$link", a_content)
+ send_message (contact_email, a_to, parameters.contact_subject_password, l_message)
+ end
+
+ send_contact_welcome_email (a_to, a_content: READABLE_STRING_8)
+ -- Send successful contact message `a_token' to `a_to'.
+ require
+ attached_to: a_to /= Void
+ local
+ l_message: STRING
+ do
+ create l_message.make_from_string (parameters.account_welcome)
+ l_message.replace_substring_all ("$link", a_content)
+ send_message (contact_email, a_to, parameters.contact_subject_oauth, l_message)
+ end
+
+
+end
diff --git a/modules/auth/site/auth.json b/modules/auth/site/auth.json
new file mode 100644
index 0000000..2f3e155
--- /dev/null
+++ b/modules/auth/site/auth.json
@@ -0,0 +1,8 @@
+{
+ "email": "webmaster@example.com",
+ "subjet_register": "Thank you for regitering with us, activate account",
+ "subjet_activate": "New account ativation token",
+ "subjet_password": "Password Recovery!!!",
+ "subjet_oauth": "Welcome",
+ "smtp": "127.0.0.1"
+}
diff --git a/modules/auth/site/mail_templates/account_activation.html b/modules/auth/site/mail_templates/account_activation.html
new file mode 100644
index 0000000..0ab4c4f
--- /dev/null
+++ b/modules/auth/site/mail_templates/account_activation.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Activation
+
+
+
+
+
+ Thank you for registering at ROC CMS
+
+ To complete your registration, please click on this link to activate your account:
+
+
$link
+ Thank you for joining us.
+
+
diff --git a/modules/auth/site/mail_templates/account_new_password.html b/modules/auth/site/mail_templates/account_new_password.html
new file mode 100644
index 0000000..ad2792d
--- /dev/null
+++ b/modules/auth/site/mail_templates/account_new_password.html
@@ -0,0 +1,17 @@
+
+
+
+
+ New Password
+
+
+
+
+
+ You have required a new password at ROC CMS
+
+ To complete your request, please click on this link to genereate a new password:
+
+
$link
+
+
diff --git a/modules/auth/site/mail_templates/account_re_activation.html b/modules/auth/site/mail_templates/account_re_activation.html
new file mode 100644
index 0000000..3590804
--- /dev/null
+++ b/modules/auth/site/mail_templates/account_re_activation.html
@@ -0,0 +1,18 @@
+
+
+
+
+ New Activation
+
+
+
+
+
+ You have request a new activation token at ROC CMS
+
+ To complete your registration, please click on this link to activate your account:
+
+
$link
+ Thank you for joining us.
+
+
diff --git a/modules/auth/site/mail_templates/account_welcome.html b/modules/auth/site/mail_templates/account_welcome.html
new file mode 100644
index 0000000..facecee
--- /dev/null
+++ b/modules/auth/site/mail_templates/account_welcome.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Welcome
+
+
+
+
+ Welcome toROC CMS
+ Thank you for joining us.
+
+
diff --git a/modules/auth/site/templates/block_account_info.tpl b/modules/auth/site/templates/block_account_info.tpl
new file mode 100644
index 0000000..0d206bb
--- /dev/null
+++ b/modules/auth/site/templates/block_account_info.tpl
@@ -0,0 +1,67 @@
+
+ {if isset="$user"}
+
Account Information
+
+
+
+ Username: {$user.name/}
+
+
+ Email: {$user.email/}
+
+
+ Creation Date: {$user.creation_date/}
+
+
+ Last login: {$user.last_login_date/}
+
+
+
+
+
+
+
+ {include file="block_change_password.tpl" /}
+
+
Roles
+
+ {foreach item="ic" from="$roles"}
+
+
+
+ {$ic.name/}
+
+ permissions
+
+ {foreach item="ip" from="$ic.permissions"}
+ {$ip/}
+ {/foreach}
+
+
+
+
+
+
+ {/foreach}
+
+
+
+
+
Profile
+
+ {foreach item="the_value" key="the_name" from="$user.profile"}
+
+ {$the_name/}: {$the_value/}
+
+ {/foreach}
+
+ {/if}
+ {unless isset="$user"}
+
+ {/unless}
+
diff --git a/modules/auth/site/templates/block_change_password.tpl b/modules/auth/site/templates/block_change_password.tpl
new file mode 100644
index 0000000..047508c
--- /dev/null
+++ b/modules/auth/site/templates/block_change_password.tpl
@@ -0,0 +1,21 @@
+
diff --git a/modules/auth/site/templates/block_login.tpl b/modules/auth/site/templates/block_login.tpl
new file mode 100644
index 0000000..fd24464
--- /dev/null
+++ b/modules/auth/site/templates/block_login.tpl
@@ -0,0 +1,29 @@
+
+ {unless isset="$user"}
+
+
+
+ {/unless}
+
diff --git a/modules/auth/site/templates/block_new_password.tpl b/modules/auth/site/templates/block_new_password.tpl
new file mode 100644
index 0000000..645bb2d
--- /dev/null
+++ b/modules/auth/site/templates/block_new_password.tpl
@@ -0,0 +1,32 @@
+
diff --git a/modules/auth/site/templates/block_post_change.tpl b/modules/auth/site/templates/block_post_change.tpl
new file mode 100644
index 0000000..bd80bf3
--- /dev/null
+++ b/modules/auth/site/templates/block_post_change.tpl
@@ -0,0 +1,3 @@
+
+
You new password has been saved!, Login again
+
diff --git a/modules/auth/site/templates/block_post_password.tpl b/modules/auth/site/templates/block_post_password.tpl
new file mode 100644
index 0000000..0ec7a7c
--- /dev/null
+++ b/modules/auth/site/templates/block_post_password.tpl
@@ -0,0 +1,3 @@
+
+
We have send you a new token code, check your email to generate a new password
+
diff --git a/modules/auth/site/templates/block_post_reactivate.tpl b/modules/auth/site/templates/block_post_reactivate.tpl
new file mode 100644
index 0000000..09e7206
--- /dev/null
+++ b/modules/auth/site/templates/block_post_reactivate.tpl
@@ -0,0 +1,3 @@
+
+
We have send you a new activation code, check your email to activate your account.
+
diff --git a/modules/auth/site/templates/block_post_register.tpl b/modules/auth/site/templates/block_post_register.tpl
new file mode 100644
index 0000000..d59f75a
--- /dev/null
+++ b/modules/auth/site/templates/block_post_register.tpl
@@ -0,0 +1,3 @@
+
+
Thanks for register, check your email to activate your account.
+
diff --git a/modules/auth/site/templates/block_post_reset.tpl b/modules/auth/site/templates/block_post_reset.tpl
new file mode 100644
index 0000000..9ccecfb
--- /dev/null
+++ b/modules/auth/site/templates/block_post_reset.tpl
@@ -0,0 +1,3 @@
+
+
You new password has been saved!
+
diff --git a/modules/auth/site/templates/block_reactivate.tpl b/modules/auth/site/templates/block_reactivate.tpl
new file mode 100644
index 0000000..0146aa2
--- /dev/null
+++ b/modules/auth/site/templates/block_reactivate.tpl
@@ -0,0 +1,19 @@
+
diff --git a/modules/auth/site/templates/block_register.tpl b/modules/auth/site/templates/block_register.tpl
new file mode 100644
index 0000000..9e7c478
--- /dev/null
+++ b/modules/auth/site/templates/block_register.tpl
@@ -0,0 +1,28 @@
+
diff --git a/modules/auth/site/templates/block_reset_password.tpl b/modules/auth/site/templates/block_reset_password.tpl
new file mode 100644
index 0000000..0101587
--- /dev/null
+++ b/modules/auth/site/templates/block_reset_password.tpl
@@ -0,0 +1,28 @@
+
diff --git a/modules/basic_auth/basic_auth-safe.ecf b/modules/basic_auth/basic_auth-safe.ecf
new file mode 100644
index 0000000..6167582
--- /dev/null
+++ b/modules/basic_auth/basic_auth-safe.ecf
@@ -0,0 +1,25 @@
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/basic_auth/cms_basic_auth_module.e b/modules/basic_auth/cms_basic_auth_module.e
new file mode 100644
index 0000000..43c0bf7
--- /dev/null
+++ b/modules/basic_auth/cms_basic_auth_module.e
@@ -0,0 +1,227 @@
+note
+ description: "[
+ This module allows the use of HTTP Basic Authentication to restrict access
+ by looking up users in the given providers.
+ ]"
+ date: "$Date: 2015-02-09 22:29:56 +0100 (lun., 09 févr. 2015) $"
+ revision: "$Revision: 96596 $"
+
+class
+ CMS_BASIC_AUTH_MODULE
+
+inherit
+ CMS_MODULE
+ redefine
+ filters,
+ setup_hooks
+ end
+
+ CMS_HOOK_AUTO_REGISTER
+
+ CMS_HOOK_BLOCK
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_VALUE_TABLE_ALTER
+
+ SHARED_LOGGER
+
+ CMS_REQUEST_UTIL
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ do
+ version := "1.0"
+ description := "Service to manage basic authentication"
+ package := "authentication"
+ add_dependency ({CMS_AUTHENTICATION_MODULE})
+ end
+
+feature -- Access
+
+ name: STRING = "basic_auth"
+
+feature -- Access: router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ configure_api_login (a_api, a_router)
+ configure_api_logoff (a_api, a_router)
+ a_router.handle ("/account/roc-basic-auth", create {WSF_URI_AGENT_HANDLER}.make (agent handle_login_basic_auth (a_api, ?, ?)), a_router.methods_head_get)
+ end
+
+feature -- Access: filter
+
+ filters (a_api: CMS_API): detachable LIST [WSF_FILTER]
+ -- Possibly list of Filter's module.
+ do
+ create {ARRAYED_LIST [WSF_FILTER]} Result.make (2)
+ Result.extend (create {CMS_CORS_FILTER})
+ Result.extend (create {CMS_BASIC_AUTH_FILTER}.make (a_api))
+ end
+
+feature {NONE} -- Implementation: routes
+
+ configure_api_login (api: CMS_API; a_router: WSF_ROUTER)
+ local
+ l_bal_handler: CMS_BASIC_AUTH_LOGIN_HANDLER
+ l_methods: WSF_REQUEST_METHODS
+ do
+ create l_bal_handler.make (api)
+ create l_methods
+ l_methods.enable_get
+ a_router.handle ("/basic_auth_login", l_bal_handler, l_methods)
+ end
+
+ configure_api_logoff (api: CMS_API; a_router: WSF_ROUTER)
+ local
+ l_bal_handler: CMS_BASIC_AUTH_LOGOFF_HANDLER
+ l_methods: WSF_REQUEST_METHODS
+ do
+ create l_bal_handler.make (api)
+ create l_methods
+ l_methods.enable_get
+ a_router.handle ("/basic_auth_logoff", l_bal_handler, l_methods)
+ end
+
+
+ handle_login_basic_auth (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_value ("Basic Auth", "optional_content_type")
+ r.execute
+ end
+
+feature -- Hooks configuration
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ -- Module hooks configuration.
+ do
+ auto_subscribe_to_hooks (a_hooks)
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_value_table_alter_hook (Current)
+ end
+
+feature -- Hooks
+
+ value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE)
+ --
+ do
+ if a_response.is_authenticated then
+ a_value.force ("basic_auth_logoff", "auth_login_strategy")
+ end
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ -- Hook execution on collection of menu contained by `a_menu_system'
+ -- for related response `a_response'.
+ local
+ lnk: CMS_LOCAL_LINK
+ lnk2: detachable CMS_LINK
+ do
+ if attached a_response.user as u then
+ across
+ a_menu_system.primary_menu.items as ic
+ until
+ lnk2 /= Void
+ loop
+ if ic.item.location.same_string ("account/roc-logout") then
+ lnk2 := ic.item
+ end
+ end
+
+ if lnk2 /= Void then
+ a_menu_system.primary_menu.remove (lnk2)
+ end
+
+ create lnk.make ("Logout", "basic_auth_logoff")
+ lnk.set_weight (98)
+ a_menu_system.primary_menu.extend (lnk)
+ else
+ if a_response.location.starts_with ("account/") then
+ create lnk.make ("Basic Auth", "account/roc-basic-auth")
+ lnk.set_expandable (True)
+ a_response.add_to_primary_tabs (lnk)
+ end
+ end
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ local
+ l_string: STRING
+ do
+ Result := <<"login">>
+ debug ("roc")
+ create l_string.make_empty
+ across
+ Result as ic
+ loop
+ l_string.append (ic.item)
+ l_string.append_character (' ')
+ end
+ write_debug_log (generator + ".block_list:" + l_string )
+ end
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if
+ a_block_id.is_case_insensitive_equal_general ("login") and then
+ a_response.location.starts_with ("account/roc-basic-auth")
+ then
+ a_response.add_javascript_url (a_response.url ("module/" + name + "/files/js/roc_basic_auth.js", Void))
+ get_block_view_login (a_block_id, a_response)
+ end
+ end
+
+feature {NONE} -- Helpers
+
+ template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK
+ -- Smarty content block for `a_block_id'
+ local
+ p: detachable PATH
+ do
+ create p.make_from_string ("templates")
+ p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl")
+
+ p := a_response.api.module_theme_resource_location (Current, p)
+ if p /= Void then
+ if attached p.entry as e then
+ create Result.make (a_block_id, Void, p.parent, e)
+ else
+ create Result.make (a_block_id, Void, p.parent, p)
+ end
+ end
+ end
+
+feature {NONE} -- Block views
+
+ get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ local
+ vals: CMS_VALUE_TABLE
+ do
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ create vals.make (1)
+ -- add the variable to the block
+ value_table_alter (vals, a_response)
+ across
+ vals as ic
+ loop
+ l_tpl_block.set_value (ic.item, ic.key)
+ end
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+
+end
diff --git a/modules/basic_auth/filter/cms_basic_auth_filter.e b/modules/basic_auth/filter/cms_basic_auth_filter.e
new file mode 100644
index 0000000..2b866ff
--- /dev/null
+++ b/modules/basic_auth/filter/cms_basic_auth_filter.e
@@ -0,0 +1,62 @@
+note
+ description: "[
+ Processes a HTTP request's BASIC authorization headers, putting the result into the execution variable user.
+ ]"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_BASIC_AUTH_FILTER
+
+inherit
+ WSF_URI_TEMPLATE_HANDLER
+ CMS_HANDLER
+ WSF_FILTER
+
+create
+ make
+
+feature -- Basic operations
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute the filter.
+ local
+ l_auth: HTTP_AUTHORIZATION
+ do
+ api.logger.put_debug (generator + ".execute ", Void)
+ create l_auth.make (req.http_authorization)
+ debug
+ if attached req.raw_header_data as l_raw_data then
+ api.logger.put_debug (generator + ".execute " + (create {UTF_CONVERTER}).escaped_utf_32_string_to_utf_8_string_8 (l_raw_data), Void)
+ end
+ end
+ -- A valid user
+ if
+ (attached l_auth.type as l_auth_type and then l_auth_type.is_case_insensitive_equal_general ("basic")) and then
+ attached l_auth.login as l_auth_login and then attached l_auth.password as l_auth_password
+ then
+ if api.user_api.is_valid_credential (l_auth_login, l_auth_password) then
+ if attached api.user_api.user_by_name (l_auth_login) as l_user then
+ debug ("refactor_fixme")
+ fixme ("Maybe we need to store in the credentials in a shared context SECURITY_CONTEXT")
+ -- req.set_execution_variable ("security_content", create SECURITY_CONTEXT.make (l_user))
+ -- other authentication filters (OpenID, etc) should implement the same approach.
+ end
+ set_current_user (req, l_user)
+ execute_next (req, res)
+ else
+ debug ("refactor_fixme")
+ to_implement ("Internal server error")
+ end
+ end
+ else
+ api.logger.put_error (generator + ".execute login_valid failed for: " + l_auth_login, Void)
+ execute_next (req, res)
+ end
+ else
+ api.logger.put_debug (generator + ".execute without authentication", Void)
+ execute_next (req, res)
+ end
+ end
+
+end
diff --git a/modules/basic_auth/filter/cms_cors_filter.e b/modules/basic_auth/filter/cms_cors_filter.e
new file mode 100644
index 0000000..9c8a151
--- /dev/null
+++ b/modules/basic_auth/filter/cms_cors_filter.e
@@ -0,0 +1,28 @@
+note
+ description: "CORS filter"
+ date: "$Date: 2014-11-13 16:23:47 +0100 (jeu., 13 nov. 2014) $"
+ revision: "$Revision: 96085 $"
+
+class
+ CMS_CORS_FILTER
+
+inherit
+
+ WSF_FILTER
+
+feature -- Basic operations
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute the filter.
+ local
+ l_header: HTTP_HEADER
+ do
+ create l_header.make
+-- l_header.add_header_key_value ("Access-Control-Allow-Origin", "localhost")
+ l_header.add_header_key_value ("Access-Control-Allow-Headers", "*")
+ l_header.add_header_key_value ("Access-Control-Allow-Methods", "*")
+ l_header.add_header_key_value ("Access-Control-Allow-Credentials", "true")
+ res.put_header_lines (l_header)
+ execute_next (req, res)
+ end
+end
diff --git a/modules/basic_auth/handler/cms_basic_auth_login_handler.e b/modules/basic_auth/handler/cms_basic_auth_login_handler.e
new file mode 100644
index 0000000..00a4ba4
--- /dev/null
+++ b/modules/basic_auth/handler/cms_basic_auth_login_handler.e
@@ -0,0 +1,70 @@
+note
+ description: "Summary description for {CMS_BASIC_AUTH_LOGIN_HANDLER}."
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_BASIC_AUTH_LOGIN_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_FILTER
+
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler.
+ do
+ execute_methods (req, res)
+ execute_next (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler.
+ do
+ execute_methods (req, res)
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ do
+ api.logger.put_information (generator + ".do_get Processing basic auth login", Void)
+ if attached {STRING_32} current_user_name (req) as l_user then
+ if attached {WSF_STRING} req.query_parameter ("destination") as l_uri then
+ redirect_to (req.absolute_script_url (l_uri.url_encoded_value), res)
+ else
+ redirect_to (req.absolute_script_url ("/"), res)
+ end
+ else
+ send_basic_authentication_challenge (Void, res)
+ end
+ end
+
+feature -- Helpers
+
+ send_basic_authentication_challenge (a_realm: detachable READABLE_STRING_8; res: WSF_RESPONSE)
+ do
+ res.send (create {CMS_UNAUTHORIZED_RESPONSE_MESSAGE}.make_with_basic_auth_challenge (a_realm))
+ end
+
+end
diff --git a/modules/basic_auth/handler/cms_basic_auth_logoff_handler.e b/modules/basic_auth/handler/cms_basic_auth_logoff_handler.e
new file mode 100644
index 0000000..3c4dce2
--- /dev/null
+++ b/modules/basic_auth/handler/cms_basic_auth_logoff_handler.e
@@ -0,0 +1,132 @@
+note
+ description: "Summary description for {CMS_BASIC_AUTH_LOGOFF_HANDLER}."
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_BASIC_AUTH_LOGOFF_HANDLER
+
+inherit
+ CMS_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler.
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler.
+ do
+ execute_methods (req, res)
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_page: CMS_RESPONSE
+ l_url: STRING
+ i: INTEGER
+ l_message: STRING
+ do
+ api.logger.put_information (generator + ".do_get Processing basic auth logoff", Void)
+ if attached req.query_parameter ("prompt") as l_prompt then
+ unset_current_user (req)
+ send_access_denied_message (res)
+ else
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ unset_current_user (req)
+ l_page.set_status_code ({HTTP_STATUS_CODE}.unauthorized) -- Note: can not use {HTTP_STATUS_CODE}.unauthorized for redirection
+ l_url := req.absolute_script_url ("")
+ i := l_url.substring_index ("://", 1)
+ if i > 0 then
+ -- Note: this is a hack to have the logout effective on various browser
+ -- (firefox requires this).
+ l_url.replace_substring ("://_logout_basic_auth_@", i, i + 2)
+ end
+ if
+ attached req.http_user_agent as l_user_agent and then
+ browser_name (l_user_agent).is_case_insensitive_equal_general ("Firefox")
+ then
+ -- Set status to refirect
+ -- and redirect to the host page.
+ l_page.set_status_code ({HTTP_STATUS_CODE}.found)
+ l_page.set_redirection (l_url)
+ end
+ create l_message.make_from_string (logout_message)
+ l_message.replace_substring_all ("$site_login", req.absolute_script_url ("/account/roc-login"))
+ l_message.replace_substring_all ("$site_home", req.absolute_script_url (""))
+ l_page.set_main_content (l_message)
+ l_page.execute
+ end
+ end
+
+
+ browser_name (a_user_agent: READABLE_STRING_8): READABLE_STRING_32
+ -- Browser name.
+ -- Must contain Must not contain
+ -- Firefox Firefox/xyz Seamonkey/xyz
+ -- Seamonkey Seamonkey/xyz
+ -- Chrome Chrome/xyz Chromium/xyz
+ -- Chromium Chromium/xyz
+ -- Safari Safari/xyz Chrome/xyz
+ -- Chromium/xyz
+ -- Opera OPR/xyz [1]
+ -- Opera/xyz [2]
+ -- Internet Explorer ;MSIE xyz; Internet Explorer doesn't put its name in the BrowserName/VersionNumber format
+
+ do
+ if
+ a_user_agent.has_substring ("Firefox") and then
+ not a_user_agent.has_substring ("Seamonkey")
+ then
+ Result := "Firefox"
+ elseif a_user_agent.has_substring ("Seamonkey") then
+ Result := "Seamonkey"
+ elseif a_user_agent.has_substring ("Chrome") and then not a_user_agent.has_substring ("Chromium")then
+ Result := "Chrome"
+ elseif a_user_agent.has_substring ("Chromium") then
+ Result := "Chromiun"
+ elseif a_user_agent.has_substring ("Safari") and then not (a_user_agent.has_substring ("Chrome") or else a_user_agent.has_substring ("Chromium")) then
+ Result := "Safari"
+ elseif a_user_agent.has_substring ("OPR") or else a_user_agent.has_substring ("Opera") then
+ Result := "Opera"
+ elseif a_user_agent.has_substring ("MSIE") or else a_user_agent.has_substring ("Trident")then
+ Result := "Internet Explorer"
+ else
+ Result := "Unknown"
+ end
+ end
+
+
+ feature {NONE}-- Lougout Message
+
+ logout_message: STRING = "[
+
+
You are now signed out
+
You can log in again, or go to the front page .
+
+ ]"
+
+
+end
diff --git a/modules/basic_auth/site/files/js/roc_basic_auth.js b/modules/basic_auth/site/files/js/roc_basic_auth.js
new file mode 100644
index 0000000..467bcd4
--- /dev/null
+++ b/modules/basic_auth/site/files/js/roc_basic_auth.js
@@ -0,0 +1,325 @@
+var ROC_AUTH = ROC_AUTH || { };
+
+var loginURL = "/basic_auth_login";
+var logoutURL = "/basic_auth_logoff";
+
+var userAgent = navigator.userAgent.toLowerCase();
+var firstLogIn = true;
+
+ROC_AUTH.login = function() {
+ var form = document.forms['cms_basic_auth'];
+ var username = form.username.value;
+ var password = form.password.value;
+ //var host = form.host.value;
+ var origin = window.location.origin + window.location.pathname;
+ var _login = function(){
+
+
+ if (document.getElementById('myModalFormId') !== null ) {
+ ROC_AUTH.remove ('myModalFormId');
+ }
+
+
+ if (username === "" || password === "") {
+ if (document.getElementById('myModalFormId') === null ) {
+ var newdiv = document.createElement('div');
+ newdiv.innerHTML = " Invalid Credentials";
+ newdiv.id = 'myModalFormId';
+ $(".primary-tabs").append(newdiv);
+ }
+ }else{
+
+ //Instantiate HTTP Request
+ var request = ((window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
+ request.open("GET", loginURL, true, username, password);
+ request.send(null);
+
+ //Process Response
+ request.onreadystatechange = function(){
+ if (request.readyState == 4) {
+ if (request.status==200) {
+ delete form;
+ window.location=window.location.origin;
+ }
+ else{
+ if (navigator.userAgent.toLowerCase().indexOf("firefox") != -1){
+ }
+
+ if (document.getElementById('myModalFormId') === null ) {
+ var newdiv = document.createElement('div');
+ newdiv.innerHTML = " Invalid Credentials";
+ newdiv.id = 'myModalFormId';
+ $(".primary-tabs").append(newdiv);
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ var userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf("firefox") != -1){ //TODO: check version number
+ if (firstLogIn) _login();
+ else logoff(_login);
+ }
+ else{
+ _login();
+ }
+
+ if (firstLogIn) firstLogIn = false;
+};
+
+
+ROC_AUTH.login_with_redirect = function() {
+ var form = document.forms[2];
+ var username = form.username.value;
+ var password = form.password.value;
+ var host = form.host.value;
+ var _login = function(){
+
+ var redirectURL = form.redirect && form.redirect.value || "";
+
+
+ $("#imgProgressRedirect").show();
+
+ if (document.getElementById('myModalFormId') !== null ) {
+ ROC_AUTH.remove ('myModalFormId');
+ }
+
+
+ if (username === "" || password === "") {
+ if (document.getElementById('myModalFormId') === null ) {
+ var newdiv = document.createElement('div');
+ newdiv.innerHTML = " Invalid Credentials";
+ newdiv.id = 'myModalFormId';
+ $(".primary-tabs").append(newdiv);
+ $("#imgProgressRedirect").hide();
+ }
+ }else{
+
+ //Instantiate HTTP Request
+ var request = ((window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
+ request.open("GET", host.concat(loginURL), true, username, password);
+ request.send(null);
+
+ //Process Response
+ request.onreadystatechange = function(){
+ if (request.readyState == 4) {
+ if (request.status==200) {
+ if (redirectURL === "") {
+ window.location=host.concat("/");
+ } else {
+ window.location=host.concat(redirectURL);
+ }
+
+ }
+ else{
+ if (navigator.userAgent.toLowerCase().indexOf("firefox") != -1){
+ }
+
+ if (document.getElementById('myModalFormId') === null ) {
+ var newdiv = document.createElement('div');
+ newdiv.innerHTML = " Invalid Credentials";
+ newdiv.id = 'myModalFormId';
+ $(".primary-tabs").append(newdiv);
+ $("#imgProgressRedirect").hide();
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ var userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf("firefox") != -1){ //TODO: check version number
+ if (firstLogIn) _login();
+ else logoff(_login);
+ }
+ else{
+ _login();
+ }
+
+ if (firstLogIn) firstLogIn = false;
+};
+
+
+ROC_AUTH.getQueryParameterByName = function (name) {
+ name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+ var regex = new RegExp("[\\?&]" + name + "=([^]*)"),
+ results = regex.exec(location.search);
+ return results === null ? " " : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
+
+ROC_AUTH.logoff = function(callback){
+ var form = document.forms[0];
+ var host = form.host.value;
+
+ if (userAgent.indexOf("msie") != -1) {
+ document.execCommand("ClearAuthenticationCache");
+ }
+ else if (userAgent.indexOf("firefox") != -1){ //TODO: check version number
+
+ var request1 = new XMLHttpRequest();
+ var request2 = new XMLHttpRequest();
+
+ //Logout. Tell the server not to return the "WWW-Authenticate" header
+ request1.open("GET", host.concat(logoutURL) + "?prompt=false", true);
+ request1.send("");
+ request1.onreadystatechange = function(){
+ if (request1.readyState == 4) {
+
+ //Sign in with dummy credentials to clear the auth cache
+ request2.open("GET", host.concat(logoutURL), true, "logout", "logout");
+ request2.send("");
+
+ request2.onreadystatechange = function(){
+ if (request2.readyState == 4) {
+ if (callback!=null) { callback.call(); } else { window.location=host.concat(logoutURL);}
+ }
+ }
+
+ }
+ }
+ }
+ else {
+ var request = ((window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
+ request.open("GET", host.concat(logoutURL), true, "logout", "logout");
+ request.send("");
+ request.onreadystatechange = function(){
+ if (request.status==401 || request.status==403 ) { window.location=host.concat(logoutURL);
+ }
+ }
+ }
+};
+
+
+ROC_AUTH.remove = function (id)
+{
+ var element = document.getElementById(id);
+ element.outerHTML = "";
+ delete element;
+ return;
+};
+
+
+
+$(document).ready(function() {
+
+ if (typeof String.prototype.contains != 'function') {
+ String.prototype.contains = function (str){
+ return this.indexOf(str) != -1;
+ };
+ }
+ ROC_AUTH.progressive_loging();
+
+});
+
+
+ROC_AUTH.progressive_loging = function () {
+
+ ROC_AUTH.login_href();
+};
+
+
+$(document).keypress(function(e) {
+ if ((e.which === 13) && (e.target.localName === 'input' && e.target.id === 'password')) {
+ ROC_AUTH.login();
+ }
+});
+
+ROC_AUTH.OnOneClick = function(event) {
+ event.preventDefault();
+ if ( document.forms[0] === undefined ) {
+ ROC_AUTH.create_form();
+ }
+ return false;
+};
+
+ROC_AUTH.login_href = function() {
+ var els = document.getElementsByTagName("a");
+ for (var i = 0, l = els.length; i < l; i++) {
+ var el = els[i];
+ if (el.href.contains("/basic_auth_login?destination")) {
+ loginURL = el.href;
+ var OneClick = el;
+ OneClick.addEventListener('click', ROC_AUTH.OnOneClick, false);
+ }
+ }
+};
+
+
+ROC_AUTH.create_form = function() {
+
+ // Fetching HTML Elements in Variables by ID.
+ var createform = document.createElement('form'); // Create New Element Form
+ createform.setAttribute("action", ""); // Setting Action Attribute on Form
+ createform.setAttribute("method", "post"); // Setting Method Attribute on Form
+ $("body").append(createform);
+
+ var heading = document.createElement('h2'); // Heading of Form
+ heading.innerHTML = "Login Form ";
+ createform.appendChild(heading);
+
+ var line = document.createElement('hr'); // Giving Horizontal Row After Heading
+ createform.appendChild(line);
+
+ var linebreak = document.createElement('br');
+ createform.appendChild(linebreak);
+
+ var namelabel = document.createElement('label'); // Create Label for Name Field
+ namelabel.innerHTML = "Username : "; // Set Field Labels
+ createform.appendChild(namelabel);
+
+ var inputelement = document.createElement('input'); // Create Input Field for UserName
+ inputelement.setAttribute("type", "text");
+ inputelement.setAttribute("name", "username");
+ inputelement.setAttribute("required","required");
+ createform.appendChild(inputelement);
+
+ var linebreak = document.createElement('br');
+ createform.appendChild(linebreak);
+
+ var passwordlabel = document.createElement('label'); // Create Label for Password Field
+ passwordlabel.innerHTML = "Password : ";
+ createform.appendChild(passwordlabel);
+
+ var passwordelement = document.createElement('input'); // Create Input Field for Password.
+ passwordelement.setAttribute("type", "password");
+ passwordelement.setAttribute("name", "password");
+ passwordelement.setAttribute("id", "password");
+ passwordelement.setAttribute("required","required");
+ createform.appendChild(passwordelement);
+
+
+ var passwordbreak = document.createElement('br');
+ createform.appendChild(passwordbreak);
+
+
+ var submitelement = document.createElement('button'); // Append Submit Button
+ submitelement.setAttribute("type", "button");
+ submitelement.setAttribute("onclick", "ROC_AUTH.login();");
+ submitelement.innerHTML = "Sign In ";
+ createform.appendChild(submitelement);
+
+};
+
+
+var password = document.getElementById("password");
+var confirm_password = document.getElementById("confirm_password");
+
+ROC_AUTH.validatePassword =function(){
+ if ((password != null) && (confirm_password != null)) {
+ if(password.value != confirm_password.value) {
+ confirm_password.setCustomValidity("Passwords Don't Match");
+ } else {
+ confirm_password.setCustomValidity('');
+ }
+ }
+}
+
+if ((password != null) && (confirm_password != null)) {
+ password.onchange = ROC_AUTH.validatePassword();
+ confirm_password.onkeyup = ROC_AUTH.validatePassword;
+}
diff --git a/modules/basic_auth/site/templates/block_login.tpl b/modules/basic_auth/site/templates/block_login.tpl
new file mode 100644
index 0000000..c49cf2c
--- /dev/null
+++ b/modules/basic_auth/site/templates/block_login.tpl
@@ -0,0 +1,29 @@
+
+ {unless isset="$user"}
+
+
+
+ {/unless}
+
diff --git a/modules/blog/cms_blog.e b/modules/blog/cms_blog.e
new file mode 100644
index 0000000..78a0d14
--- /dev/null
+++ b/modules/blog/cms_blog.e
@@ -0,0 +1,112 @@
+note
+ description: "Summary description for {CMS_BLOG}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_BLOG
+
+inherit
+ CMS_NODE
+ redefine
+ make_empty,
+ import_node
+ end
+
+create
+ make_empty,
+ make
+
+feature {NONE} -- Initialization
+
+ make_empty
+ do
+ Precursor
+ end
+
+feature -- Conversion
+
+ import_node (a_node: CMS_NODE)
+ --
+ do
+ Precursor (a_node)
+ if attached {CMS_BLOG} a_node as l_blog then
+ if attached l_blog.tags as l_tags then
+ across
+ l_tags as ic
+ loop
+ add_tag (ic.item)
+ end
+ end
+ end
+ end
+
+feature -- Access
+
+ content_type: READABLE_STRING_8
+ once
+ Result := {CMS_BLOG_NODE_TYPE}.name
+ end
+
+feature -- Access: node
+
+ summary: detachable READABLE_STRING_32
+ -- A short summary of the node.
+
+ content: detachable READABLE_STRING_32
+ -- Content of the node.
+
+ format: detachable READABLE_STRING_8
+ -- Format associated with `content' and `summary'.
+ -- For example: text, mediawiki, html, etc
+
+feature -- Access: blog
+
+ tags: detachable ARRAYED_LIST [READABLE_STRING_32]
+ -- Optional tags
+
+feature -- Element change: node
+
+ set_content (a_content: like content; a_summary: like summary; a_format: like format)
+ do
+ content := a_content
+ summary := a_summary
+ format := a_format
+ end
+
+feature -- Element change: blog
+
+ add_tag (a_tag: READABLE_STRING_32)
+ -- Set `parent' to `a_page'
+ require
+ not a_tag.is_whitespace
+ local
+ l_tags: like tags
+ do
+ l_tags := tags
+ if l_tags = Void then
+ create l_tags.make (1)
+ tags := l_tags
+ end
+ l_tags.force (a_tag)
+ end
+
+ set_tags_from_string (a_tags: READABLE_STRING_32)
+ local
+ t: STRING_32
+ do
+ tags := Void
+ across
+ a_tags.split (',') as ic
+ loop
+ t := ic.item
+ t.left_adjust
+ t.right_adjust
+ if not t.is_whitespace then
+ add_tag (t)
+ end
+ end
+ end
+
+end
+
diff --git a/modules/blog/cms_blog_api.e b/modules/blog/cms_blog_api.e
new file mode 100644
index 0000000..695289d
--- /dev/null
+++ b/modules/blog/cms_blog_api.e
@@ -0,0 +1,133 @@
+note
+ description: "API to handle nodes of type blog. Extends the node API."
+ author: "Dario Bösch
+ do
+ Precursor
+
+ -- Create the node storage for type blog
+ if attached storage.as_sql_storage as l_storage_sql then
+ create {CMS_BLOG_STORAGE_SQL} blog_storage.make (l_storage_sql)
+ else
+ create {CMS_BLOG_STORAGE_NULL} blog_storage.make
+ end
+-- initialize_node_types
+ end
+
+feature {CMS_API_ACCESS, CMS_MODULE, CMS_API} -- Restricted access
+
+ node_api: CMS_NODE_API
+
+feature {CMS_MODULE} -- Access nodes storage.
+
+ blog_storage: CMS_BLOG_STORAGE_I
+
+feature -- Configuration of blog handlers
+
+ entries_per_page : NATURAL_32 = 2
+ -- The numbers of posts that are shown on one page. If there are more post a pagination is generated
+ --| For test reasons this is 2, so we don't have to create a lot of blog entries.
+ --| TODO: Set to bigger constant.
+
+feature -- Access node
+
+ blogs_count: INTEGER_64
+ -- Number of nodes of type blog.
+ do
+ Result := blog_storage.blogs_count
+ end
+
+ blogs_count_from_user (a_user: CMS_USER): INTEGER_64
+ -- Number of nodes of type blog from user with `a_user_id'.
+ require
+ has_id: a_user.has_id
+ do
+ Result := blog_storage.blogs_count_from_user (a_user)
+ end
+
+ blogs_order_created_desc: LIST [CMS_BLOG]
+ -- List of nodes ordered by creation date (descending)
+ do
+ Result := nodes_to_blogs (blog_storage.blogs)
+ end
+
+ blogs_order_created_desc_limited (a_limit: NATURAL_32; a_offset: NATURAL_32): LIST [CMS_BLOG]
+ -- List of nodes ordered by creation date and limited by limit and offset
+ do
+ -- load all posts and add the authors to each post
+ Result := nodes_to_blogs (blog_storage.blogs_limited (a_limit, a_offset))
+ end
+
+ blogs_from_user_order_created_desc_limited (a_user: CMS_USER; a_limit: NATURAL_32; a_offset: NATURAL_32) : LIST [CMS_BLOG]
+ -- List of nodes ordered by creation date and limited by limit and offset
+ require
+ has_id: a_user.has_id
+ do
+ -- load all posts and add the authors to each post
+ Result := nodes_to_blogs (blog_storage.blogs_from_user_limited (a_user, a_limit, a_offset))
+ end
+
+feature -- Conversion
+
+ full_blog_node (a_blog: CMS_BLOG): CMS_BLOG
+ -- If `a_blog' is partial, return the full blog node from `a_blog',
+ -- otherwise return directly `a_blog'.
+ require
+ a_blog_set: a_blog /= Void
+ do
+ if attached {CMS_BLOG} node_api.full_node (a_blog) as l_full_blog then
+ Result := l_full_blog
+ else
+ Result := a_blog
+ end
+ end
+
+feature {NONE} -- Helpers
+
+ nodes_to_blogs (a_nodes: LIST [CMS_NODE]): ARRAYED_LIST [CMS_BLOG]
+ -- Convert list of nodes into a list of blog when possible.
+ do
+ create {ARRAYED_LIST [CMS_BLOG]} Result.make (a_nodes.count)
+
+ if attached node_api as l_node_api then
+ across
+ a_nodes as ic
+ loop
+ if attached {CMS_BLOG} l_node_api.full_node (ic.item) as l_blog then
+ Result.force (l_blog)
+ end
+ end
+ end
+ end
+
+end
diff --git a/modules/blog/cms_blog_module-safe.ecf b/modules/blog/cms_blog_module-safe.ecf
new file mode 100644
index 0000000..72f9a73
--- /dev/null
+++ b/modules/blog/cms_blog_module-safe.ecf
@@ -0,0 +1,30 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/blog/cms_blog_module.e b/modules/blog/cms_blog_module.e
new file mode 100644
index 0000000..42d283e
--- /dev/null
+++ b/modules/blog/cms_blog_module.e
@@ -0,0 +1,246 @@
+note
+ description: "Displays all posts (pages with type blog). It's possible to list posts by user."
+ author: "Dario Bösch "
+ date: "$Date: 2015-05-22 15:13:00 +0100 (lun., 18 mai 2015) $"
+ revision: "$Revision 96616$"
+
+class
+ CMS_BLOG_MODULE
+
+inherit
+ CMS_MODULE
+ rename
+ module_api as blog_api
+ redefine
+ setup_hooks,
+ initialize,
+ install,
+ blog_api
+ end
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_RESPONSE_ALTER
+
+ CMS_HOOK_EXPORT
+
+ CMS_EXPORT_NODE_UTILITIES
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ do
+ version := "1.0"
+ description := "Service to demonstrate new node for blog"
+ package := "demo"
+ add_dependency ({CMS_NODE_MODULE})
+ end
+
+feature -- Access
+
+ name: STRING = "blog"
+
+feature {CMS_API} -- Module Initialization
+
+ initialize (api: CMS_API)
+ --
+ local
+ ct: CMS_BLOG_NODE_TYPE
+ do
+ Precursor (api)
+
+ if attached {CMS_NODE_API} api.module_api ({CMS_NODE_MODULE}) as l_node_api then
+ create ct
+ create blog_api.make (api, l_node_api)
+
+ node_api := l_node_api
+ -- Depends on {CMS_NODE_MODULE}
+
+ --| For now, add all available formats to content type `ct'.
+ across
+ api.formats as ic
+ loop
+ ct.extend_format (ic.item)
+ end
+ l_node_api.add_node_type (ct)
+ l_node_api.add_node_type_webform_manager (create {CMS_BLOG_NODE_TYPE_WEBFORM_MANAGER}.make (ct, l_node_api))
+
+ -- Add support for CMS_BLOG, which requires a storage extension to store the optional "tags" value
+ -- For now, we only have extension based on SQL statement.
+ if attached {CMS_NODE_STORAGE_SQL} l_node_api.node_storage as l_sql_node_storage then
+ l_sql_node_storage.register_node_storage_extension (create {CMS_NODE_STORAGE_SQL_BLOG_EXTENSION}.make (l_node_api, l_sql_node_storage))
+ end
+ end
+ end
+
+feature {CMS_API} -- Module management
+
+ install (api: CMS_API)
+ local
+ sql: STRING
+ do
+ -- Schema
+ if attached api.storage.as_sql_storage as l_sql_storage then
+ if not l_sql_storage.sql_table_exists ("blog_post_nodes") then
+ sql := "[
+CREATE TABLE blog_post_nodes(
+ `nid` INTEGER NOT NULL CHECK("nid">=0),
+ `revision` INTEGER NOT NULL,
+ `tags` VARCHAR(255),
+ CONSTRAINT PK_nid_revision PRIMARY KEY (nid,revision)
+);
+ ]"
+ l_sql_storage.sql_execute_script (sql, Void)
+ if l_sql_storage.has_error then
+ api.logger.put_error ("Could not initialize database for blog module", generating_type)
+ end
+ end
+ Precursor (api)
+ end
+ end
+
+feature {CMS_API} -- Access: API
+
+ blog_api: detachable CMS_BLOG_API
+ --
+
+ node_api: detachable CMS_NODE_API
+
+feature -- Access: router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ if attached blog_api as l_blog_api then
+ configure_web (a_api, l_blog_api, a_router)
+ else
+ -- Issue with api/dependencies,
+ -- thus Current module should not be used!
+ -- thus no url mapping
+ end
+ end
+
+ configure_web (a_api: CMS_API; a_blog_api: CMS_BLOG_API; a_router: WSF_ROUTER)
+ -- Configure router mapping for web interface.
+ local
+ l_blog_handler: BLOG_HANDLER
+ l_blog_user_handler: BLOG_USER_HANDLER
+ l_uri_mapping: WSF_URI_MAPPING
+ do
+ -- TODO: for now, focused only on web interface, add REST api later. [2015-May-18]
+ create l_blog_handler.make (a_api, a_blog_api)
+ create l_blog_user_handler.make (a_api, a_blog_api)
+
+ -- Let the class BLOG_HANDLER handle the requests on "/blogs"
+ create l_uri_mapping.make_trailing_slash_ignored ("/blogs", l_blog_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get)
+
+ -- We can add a page number after /blogs/ to get older posts
+ a_router.handle ("/blogs/page/{page}", l_blog_handler, a_router.methods_get)
+
+ -- If a user id is given route with blog user handler
+ --| FIXME: maybe /user/{user}/blogs/ would be better.
+ a_router.handle ("/blogs/user/{user}", l_blog_user_handler, a_router.methods_get)
+
+ -- If a user id is given we also want to allow different pages
+ --| FIXME: what about /user/{user}/blogs/?page={page} ?
+ a_router.handle ("/blogs/user/{user}/page/{page}", l_blog_user_handler, a_router.methods_get)
+
+ end
+
+feature -- Hooks
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ do
+ a_hooks.subscribe_to_menu_system_alter_hook (Current)
+ a_hooks.subscribe_to_response_alter_hook (Current)
+ a_hooks.subscribe_to_export_hook (Current)
+ end
+
+ response_alter (a_response: CMS_RESPONSE)
+ do
+ a_response.add_style (a_response.url ("/module/" + name + "/files/css/blog.css", Void), Void)
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ local
+ lnk: CMS_LOCAL_LINK
+ do
+ -- Add the link to the blog to the main menu
+ create lnk.make ("Blogs", "blogs/")
+ a_menu_system.primary_menu.extend (lnk)
+ end
+
+ export_to (a_export_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_export_parameters: CMS_EXPORT_PARAMETERS; a_response: CMS_RESPONSE)
+ -- Export data identified by `a_export_id_list',
+ -- or export all data if `a_export_id_list' is Void.
+ local
+ n: CMS_BLOG
+ p: PATH
+ d: DIRECTORY
+ f: PLAIN_TEXT_FILE
+ lst: LIST [CMS_BLOG]
+ do
+ if
+ a_export_id_list = Void
+ or else across a_export_id_list as ic some ic.item.same_string ("blog") end
+ then
+ if
+ a_response.has_permissions (<<"export any node", "export blog">>) and then
+ attached blog_api as l_blog_api
+ then
+ lst := l_blog_api.blogs_order_created_desc
+ a_export_parameters.log ("Exporting " + lst.count.out + " blogs")
+ across
+ lst as ic
+ loop
+ n := l_blog_api.full_blog_node (ic.item)
+ a_export_parameters.log (n.content_type + " #" + n.id.out)
+ p := a_export_parameters.location.extended ("nodes").extended (n.content_type).extended (n.id.out)
+ create d.make_with_path (p.parent)
+ if not d.exists then
+ d.recursive_create_dir
+ end
+ create f.make_with_path (p)
+ if not f.exists or else f.is_access_writable then
+ f.open_write
+ f.put_string (json_to_string (blog_node_to_json (n)))
+ f.close
+ end
+ -- Revisions.
+ if
+ attached node_api as l_node_api and then
+ attached l_node_api.node_revisions (n) as l_revisions and then l_revisions.count > 1
+ then
+ a_export_parameters.log (n.content_type + " " + l_revisions.count.out + " revisions.")
+ p := a_export_parameters.location.extended ("nodes").extended (n.content_type).extended (n.id.out)
+ create d.make_with_path (p)
+ if not d.exists then
+ d.recursive_create_dir
+ end
+ across
+ l_revisions as revs_ic
+ loop
+ if attached {CMS_BLOG} revs_ic.item as l_blog then
+ create f.make_with_path (p.extended ("rev-" + n.revision.out).appended_with_extension ("json"))
+ if not f.exists or else f.is_access_writable then
+ f.open_write
+ f.put_string (json_to_string (blog_node_to_json (l_blog)))
+ end
+ f.close
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ blog_node_to_json (a_blog: CMS_BLOG): JSON_OBJECT
+ do
+ Result := node_to_json (a_blog)
+ end
+end
diff --git a/modules/blog/cms_blog_node_type.e b/modules/blog/cms_blog_node_type.e
new file mode 100644
index 0000000..d8d713c
--- /dev/null
+++ b/modules/blog/cms_blog_node_type.e
@@ -0,0 +1,44 @@
+note
+ description: "Summary description for {CMS_BLOG_NODE_TYPE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_BLOG_NODE_TYPE
+
+inherit
+ CMS_NODE_TYPE [CMS_BLOG]
+
+feature -- Access
+
+ name: STRING = "blog"
+ -- Internal name.
+
+ title: STRING_32 = "Blog"
+ -- Human readable name.
+
+ description: STRING_32 = "Content published as a blog post."
+ -- Optional description
+
+feature -- Factory
+
+ new_node_with_title (a_title: READABLE_STRING_32; a_partial_node: detachable CMS_NODE): like new_node
+ -- New node with `a_title' and fill from partial `a_partial_node' if set.
+ do
+ create Result.make (a_title)
+ if a_partial_node /= Void then
+ Result.import_node (a_partial_node)
+ Result.set_title (a_title)
+ end
+ end
+
+ new_node (a_partial_node: detachable CMS_NODE): CMS_BLOG
+ -- New node based on partial `a_partial_node', or from none.
+ do
+ create Result.make_empty
+ if a_partial_node /= Void then
+ Result.import_node (a_partial_node)
+ end
+ end
+
+end
diff --git a/modules/blog/cms_blog_node_type_webform_manager.e b/modules/blog/cms_blog_node_type_webform_manager.e
new file mode 100644
index 0000000..7c0c211
--- /dev/null
+++ b/modules/blog/cms_blog_node_type_webform_manager.e
@@ -0,0 +1,99 @@
+note
+ description: "Summary description for {CMS_BLOG_NODE_TYPE_WEBFORM_MANAGER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_BLOG_NODE_TYPE_WEBFORM_MANAGER
+
+inherit
+ CMS_NODE_TYPE_WEBFORM_MANAGER [CMS_BLOG]
+ redefine
+ content_type,
+ populate_form,
+ update_node,
+ new_node,
+ append_content_as_html_to
+ end
+
+create
+ make
+
+feature -- Access
+
+ content_type: CMS_BLOG_NODE_TYPE
+ -- Associated content type.
+
+feature -- form
+
+ populate_form (response: NODE_RESPONSE; f: CMS_FORM; a_node: detachable CMS_NODE)
+ local
+ ti: WSF_FORM_TEXT_INPUT
+ s: STRING_32
+ do
+ Precursor (response, f, a_node)
+ create ti.make ("tags")
+ ti.set_label ("Tags")
+ ti.set_size (70)
+ if
+ a_node /= Void and then
+ attached {CMS_BLOG} a_node as a_blog and then
+ attached a_blog.tags as l_tags
+ then
+ create s.make_empty
+ across
+ l_tags as ic
+ loop
+ if not s.is_empty then
+ s.append_character (',')
+ end
+ s.append (ic.item)
+ end
+ ti.set_text_value (s)
+ end
+ ti.set_is_required (False)
+ f.extend (ti)
+ end
+
+ update_node (response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: CMS_NODE)
+ do
+ Precursor (response, fd, a_node)
+ if attached fd.string_item ("tags") as l_tags then
+ if attached {CMS_BLOG} a_node as l_blog then
+ l_blog.set_tags_from_string (l_tags)
+ end
+ end
+ end
+
+ new_node (response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: detachable like new_node): like content_type.new_node
+ --
+ do
+ Result := Precursor (response, fd, a_node)
+ if attached fd.string_item ("tags") as l_tags then
+ Result.set_tags_from_string (l_tags)
+ end
+ end
+
+feature -- Output
+
+ append_content_as_html_to (a_node: CMS_BLOG; is_teaser: BOOLEAN; a_output: STRING; a_response: detachable CMS_RESPONSE)
+ --
+ do
+ Precursor (a_node, is_teaser, a_output, a_response)
+
+ if attached {CMS_BLOG} a_node as l_blog_post then
+ if attached l_blog_post.tags as l_tags then
+ a_output.append ("Tags: ")
+ across
+ l_tags as ic
+ loop
+ a_output.append ("")
+ a_output.append (cms_api.html_encoded (ic.item))
+ a_output.append (" ")
+ end
+ a_output.append ("
")
+ end
+ end
+ end
+
+end
diff --git a/modules/blog/cms_node_storage_sql_blog_extension.e b/modules/blog/cms_node_storage_sql_blog_extension.e
new file mode 100644
index 0000000..29eef63
--- /dev/null
+++ b/modules/blog/cms_node_storage_sql_blog_extension.e
@@ -0,0 +1,167 @@
+note
+ description: "Storage extension for Blog nodes."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_NODE_STORAGE_SQL_BLOG_EXTENSION
+
+inherit
+ CMS_NODE_STORAGE_EXTENSION [CMS_BLOG]
+
+ CMS_PROXY_STORAGE_SQL
+ rename
+ make as make_proxy,
+ sql_storage as node_storage
+ redefine
+ node_storage
+ end
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_node_api: CMS_NODE_API; a_sql_storage: CMS_NODE_STORAGE_SQL)
+ do
+ set_node_api (a_node_api)
+ make_proxy (a_sql_storage)
+ end
+
+ node_storage: CMS_NODE_STORAGE_SQL
+ --
+
+feature -- Access
+
+ content_type: STRING
+ once
+ Result := {CMS_BLOG_NODE_TYPE}.name
+ end
+
+feature -- Persistence
+
+ store (a_node: CMS_BLOG)
+ -- .
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_new_tags: detachable STRING_32
+ l_previous_tags: detachable STRING_32
+ l_update: BOOLEAN
+ l_has_modif: BOOLEAN
+ do
+ if attached api as l_api then
+ l_api.logger.put_information (generator + ".store", Void)
+ end
+
+ error_handler.reset
+ -- Check existing record, if any.
+ if attached node_data (a_node) as d then
+ l_update := d.revision = a_node.revision
+ l_previous_tags := d.tags
+ end
+ if not has_error then
+ if attached a_node.tags as l_tags and then not l_tags.is_empty then
+ create l_new_tags.make (0)
+ across
+ l_tags as ic
+ loop
+ if not l_new_tags.is_empty then
+ l_new_tags.append_character (',')
+ end
+ l_new_tags.append (ic.item)
+ end
+ else
+ l_new_tags := Void
+ end
+
+ l_has_modif := l_has_modif or (l_new_tags /~ l_previous_tags)
+
+ create l_parameters.make (3)
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ l_parameters.force (l_new_tags, "tags")
+
+ if l_update then
+ if l_has_modif then
+ sql_modify (sql_update_node_data, l_parameters)
+ end
+ else
+ if l_has_modif then
+ sql_insert (sql_insert_node_data, l_parameters)
+ else
+ -- no page data, means everything is empty.
+ -- FOR NOW: always record row
+-- sql_change (sql_insert_node_data, l_parameters)
+ end
+ end
+ sql_finalize
+ end
+ end
+
+ load (a_node: CMS_BLOG)
+ -- .
+ local
+ l_tags: READABLE_STRING_32
+ do
+ if attached node_data (a_node) as d then
+ l_tags := d.tags
+ a_node.set_tags_from_string (l_tags)
+ end
+ end
+
+ delete_node (a_node: CMS_BLOG)
+ --
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ if a_node.has_id then
+ create l_parameters.make (1)
+ l_parameters.put (a_node.id, "nid")
+ sql_modify (sql_delete_node_data, l_parameters)
+ sql_finalize
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ node_data (a_node: CMS_NODE): detachable TUPLE [revision: INTEGER_64; tags: READABLE_STRING_32]
+ -- Node extension data for node `a_node' as tuple.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ l_rev: INTEGER_64
+ l_tags: detachable READABLE_STRING_32
+ n: INTEGER
+ do
+ error_handler.reset
+ create l_parameters.make (2)
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ sql_query (sql_select_node_data, l_parameters)
+ if not has_error then
+ if not sql_after then
+ -- nid, revision, tags
+ l_rev := sql_read_integer_64 (2)
+ l_tags := sql_read_string_32 (3)
+ if l_tags /= Void then
+ Result := [l_rev, l_tags]
+ end
+ sql_forth
+ if not sql_after then
+ check unique_data: n = 0 end
+ Result := Void
+ end
+ end
+ end
+ sql_finalize
+ ensure
+ accepted_revision: Result /= Void implies Result.revision <= a_node.revision
+ end
+
+feature -- SQL
+
+ sql_select_node_data: STRING = "SELECT nid, revision, tags FROM blog_post_nodes WHERE nid=:nid AND revision<=:revision ORDER BY revision DESC LIMIT 1;"
+ sql_insert_node_data: STRING = "INSERT INTO blog_post_nodes (nid, revision, tags) VALUES (:nid, :revision, :tags);"
+ sql_update_node_data: STRING = "UPDATE blog_post_nodes SET nid=:nid, revision=:revision, tags=:tags WHERE nid=:nid AND revision=:revision;"
+ sql_delete_node_data: STRING = "DELETE FROM blog_post_nodes WHERE nid=:nid;"
+
+end
diff --git a/modules/blog/handler/blog_handler.e b/modules/blog/handler/blog_handler.e
new file mode 100644
index 0000000..1e4b28e
--- /dev/null
+++ b/modules/blog/handler/blog_handler.e
@@ -0,0 +1,279 @@
+note
+ description: "Request handler related to /blogs and /blogs/{page}. Displays all posts in the blog."
+ author: "Dario Bösch "
+ date: "$Date: 2015-05-18 13:49:00 +0100 (lun., 18 mai 2015) $"
+ revision: "$9661667$"
+
+class
+ BLOG_HANDLER
+
+inherit
+ CMS_BLOG_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+ CMS_API_ACCESS
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler for any kind of mapping.
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler for URI mapping.
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler for URI-template mapping.
+ do
+ execute (req, res)
+ end
+
+feature -- Global Variables
+
+ page_number: NATURAL_32
+ -- Current page number.
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_page: CMS_RESPONSE
+ do
+ -- Read page number from path parameter.
+ page_number := page_number_path_parameter (req)
+
+ -- Responding with `main_content_html (l_page)'.
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ l_page.set_main_content (main_content_html (l_page))
+ l_page.execute
+ end
+
+feature -- Query
+
+ posts: LIST [CMS_NODE]
+ -- Blog posts to display on given page ordered by date (descending).
+ do
+ Result := blog_api.blogs_order_created_desc_limited (
+ entries_per_page,
+ entries_per_page * (page_number - 1)
+ )
+ end
+
+ multiple_pages_needed : BOOLEAN
+ -- Return if more that one page is needed to display posts.
+ do
+ Result := entries_per_page < total_entries
+ end
+
+ pages_count: NATURAL_32
+ -- Number of pages needed to display all posts.
+ require
+ entries_per_page > 0
+ local
+ tmp: REAL_32
+ do
+ tmp := total_entries.to_real_32 / entries_per_page.to_real_32;
+ Result := tmp.ceiling.to_natural_32
+ end
+
+ page_number_path_parameter (req: WSF_REQUEST): NATURAL_32
+ -- Page number from path /blogs/{page}.
+ -- Unsigned integer since negative pages are not allowed.
+ local
+ s: STRING
+ do
+ Result := 1 -- default if not get variable is set
+ if attached {WSF_STRING} req.path_parameter ("page") as p_page then
+ s := p_page.value
+ if s.is_natural_32 then
+ if s.to_natural_32 > 1 then
+ Result := s.to_natural_32
+ end
+ end
+ end
+ end
+
+ total_entries: NATURAL_32
+ -- Total number of entries/posts.
+ do
+ Result := blog_api.blogs_count.to_natural_32
+ end
+
+feature -- HTML Output
+
+ frozen main_content_html (page: CMS_RESPONSE): STRING
+ -- Content of the page as a html string.
+ do
+ create Result.make_empty
+ append_main_content_html_to (page, Result)
+ end
+
+ append_main_content_html_to (page: CMS_RESPONSE; a_output: STRING)
+ -- Append to `a_output, the content of the page as a html string.
+ local
+ n: CMS_NODE
+ lnk: CMS_LOCAL_LINK
+ do
+ -- Output the title. If more than one page, also output the current page number
+ append_page_title_html_to (a_output)
+
+ -- Get the posts from the current page (given by page number and entries per page)
+ -- Start list of posts
+ a_output.append ("%N")
+ across
+ posts as ic
+ loop
+ n := ic.item
+ lnk := blog_api.node_api.node_link (n)
+ a_output.append ("")
+
+ -- Output the creation date
+ append_creation_date_html_to (n, a_output)
+
+ -- Output the author of the post
+ append_author_html_to (n, a_output)
+
+ -- Output the title of the post as a link (to the detail page)
+ append_title_html_to (n, page, a_output)
+
+ -- Output the summary of the post and a more link to the detail page
+ append_summary_html_to (n, page, a_output)
+
+ a_output.append (" %N")
+ end
+
+ -- End of post list
+ a_output.append (" %N")
+
+ -- Pagination (older and newer links)
+ append_pagination_html_to (a_output)
+ end
+
+ append_page_title_html_to (a_output: STRING)
+ -- Append the title of the page as a html string to `a_output'.
+ -- It shows the current page number.
+ do
+ a_output.append ("Blog")
+ if multiple_pages_needed then
+ a_output.append (" (Page " + page_number.out + " of " + pages_count.out + ")")
+ end
+ a_output.append (" ")
+ end
+
+ append_creation_date_html_to (n: CMS_NODE; a_output: STRING)
+ -- Append the creation date as a html string to `a_output'.
+ local
+ hdate: HTTP_DATE
+ do
+ if attached n.creation_date as l_modified then
+ create hdate.make_from_date_time (l_modified)
+ hdate.append_to_yyyy_mmm_dd_string (a_output)
+ a_output.append (" ")
+ end
+ end
+
+ append_author_html_to (n: CMS_NODE; a_output: STRING)
+ -- Append to `a_output', the author of node `n' as html link to author's posts.
+ do
+ if attached n.author as l_author then
+ a_output.append ("by ")
+ a_output.append ("" + l_author.name + " ")
+ end
+ end
+
+ append_title_html_to (n: CMS_NODE; page: CMS_RESPONSE; a_output: STRING)
+ -- Append to `a_output', the title of node `n' as html link to detail page.
+ local
+ lnk: CMS_LOCAL_LINK
+ do
+ lnk := blog_api.node_api.node_link (n)
+ a_output.append ("")
+ a_output.append (page.link (lnk.title, lnk.location, Void))
+ a_output.append (" ")
+ end
+
+ append_summary_html_to (n: CMS_NODE; page: CMS_RESPONSE; a_output: STRING)
+ -- returns a html string with the summary of the node and a link to the detail page
+ local
+ lnk: CMS_LOCAL_LINK
+ do
+ if attached n.summary as l_summary then
+ lnk := blog_api.node_api.node_link (n)
+ a_output.append ("")
+ if attached api.format (n.format) as f then
+ f.append_formatted_to (l_summary, a_output)
+ else
+ api.formats.default_format.append_formatted_to (l_summary, a_output)
+ end
+ a_output.append (" ")
+ a_output.append (page.link ("See more...", lnk.location, Void))
+ a_output.append ("
")
+ end
+ end
+
+ append_pagination_html_to (a_output: STRING)
+ -- Append to `a_output' with the pagination links (if necessary).
+ local
+ tmp: NATURAL_32
+ do
+ if multiple_pages_needed then
+ a_output.append ("")
+ end
+
+ end
+
+ base_path : STRING
+ -- the path to the page that lists all blogs
+ do
+ Result := "/blogs"
+ end
+
+end
diff --git a/modules/blog/handler/blog_user_handler.e b/modules/blog/handler/blog_user_handler.e
new file mode 100644
index 0000000..8a50809
--- /dev/null
+++ b/modules/blog/handler/blog_user_handler.e
@@ -0,0 +1,148 @@
+note
+ description: "[
+ Request handler related to
+ /blogs/user/{id}/
+ or /blogs/user/{id}/page/{page}.
+
+ Displays all posts of the given user
+ ]"
+ author: "Dario Bösch "
+ date: "$Date: 2015-05-22 15:13:00 +0100 (lun., 18 mai 2015) $"
+ revision: "$Revision 96616$"
+
+class
+ BLOG_USER_HANDLER
+
+inherit
+ BLOG_HANDLER
+ redefine
+ do_get,
+ posts,
+ total_entries,
+ append_page_title_html_to,
+ base_path
+ end
+
+create
+ make
+
+feature -- Global Variables
+
+ user : detachable CMS_USER
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_error: NOT_FOUND_ERROR_CMS_RESPONSE
+ do
+ user := Void
+ if attached user_from_request (req) as l_user then
+ user := l_user
+ -- Output the results, similar as in the blog hanlder (but with other queries)
+ Precursor (req, res)
+ else
+ -- Throw a bad request error because the user is not valid
+ create l_error.make (req, res, api)
+ l_error.set_main_content ("Error User with id " + user_id_path_parameter (req).out + " doesn't exist!")
+ l_error.execute
+ end
+ end
+
+feature -- Query
+
+ user_valid (req: WSF_REQUEST) : BOOLEAN
+ -- Returns true if a valid user id is given and a user with this id exists,
+ -- otherwise returns false.
+ local
+ user_id: INTEGER_32
+ do
+ user_id := user_id_path_parameter (req)
+
+ if user_id <= 0 then
+ -- Given user id is not valid
+ Result := False
+ else
+ --Check if user with user_id exists
+ Result := api.user_api.user_by_id (user_id) /= Void
+ end
+ end
+
+ user_from_request (req: WSF_REQUEST): detachable CMS_USER
+ -- Eventual user with given id in the path of request `req'.
+ local
+ uid: like user_id_path_parameter
+ do
+ uid := user_id_path_parameter (req)
+ if uid > 0 then
+ Result := api.user_api.user_by_id (uid)
+ else
+ -- Missing or invalid user id.
+ end
+ end
+
+ user_id_path_parameter (req: WSF_REQUEST): INTEGER_32
+ -- User id from path /blogs/{user}.
+ -- Unsigned integer since negative ids are not allowed.
+ -- If no valid id can be read it returns -1
+ do
+ Result := -1
+ if attached {WSF_STRING} req.path_parameter ("user") as l_user_id then
+ if l_user_id.is_integer then
+ Result := l_user_id.integer_value
+ end
+ end
+ end
+
+ posts: LIST [CMS_BLOG]
+ -- Blog posts to display on given page.
+ -- Filters out the posts of the current user.
+ do
+ if attached user as l_user then
+ Result := blog_api.blogs_from_user_order_created_desc_limited (l_user, entries_per_page, entries_per_page * (page_number - 1))
+ else
+ create {ARRAYED_LIST [CMS_BLOG]} Result.make (0)
+ end
+ end
+
+ total_entries : NATURAL_32
+ -- Returns the number of total entries/posts of the current user
+ do
+ if attached user as l_user then
+ Result := blog_api.blogs_count_from_user (l_user).to_natural_32
+ else
+ Result := Precursor
+ end
+ end
+
+feature -- HTML Output
+
+ append_page_title_html_to (a_output: STRING)
+ -- Returns the title of the page as a html string. It shows the current page number and the name of the current user
+ do
+ a_output.append ("Posts from ")
+ if attached user as l_user then
+ a_output.append (html_encoded (l_user.name))
+ else
+ a_output.append ("unknown user")
+ end
+ if multiple_pages_needed then
+ a_output.append (" (Page " + page_number.out + " of " + pages_count.out + ")")
+ -- Get the posts from the current page (limited by entries per page)
+ end
+ a_output.append (" ")
+ end
+
+ base_path : STRING
+ -- Path to page listing all blogs.
+ -- If user is logged in, include user id
+ do
+ if attached user as l_user then
+ Result := "/blogs/user/" + l_user.id.out
+ else
+ Result := precursor
+ end
+ end
+
+end
diff --git a/modules/blog/handler/cms_blog_handler.e b/modules/blog/handler/cms_blog_handler.e
new file mode 100644
index 0000000..450096f
--- /dev/null
+++ b/modules/blog/handler/cms_blog_handler.e
@@ -0,0 +1,23 @@
+note
+ description: "Deferred request handler related to /blogs/... Has an own blog api."
+ author: "Dario Bösch "
+ date: "$Date: 2015-05-18 13:49:00 +0100 (lun., 18 mai 2015) $"
+ revision: "$9661667$"
+
+deferred class
+ CMS_BLOG_HANDLER
+
+inherit
+ CMS_MODULE_HANDLER [CMS_BLOG_API]
+ rename
+ module_api as blog_api
+ end
+
+feature -- Access
+
+ entries_per_page: NATURAL_32
+ do
+ Result := blog_api.entries_per_page
+ end
+
+end
diff --git a/modules/blog/persistence/cms_blog_storage_i.e b/modules/blog/persistence/cms_blog_storage_i.e
new file mode 100644
index 0000000..cee1d47
--- /dev/null
+++ b/modules/blog/persistence/cms_blog_storage_i.e
@@ -0,0 +1,47 @@
+note
+ description: "Interface for accessing blog contents from the database."
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+deferred class
+ CMS_BLOG_STORAGE_I
+
+feature -- Error Handling
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ deferred
+ end
+
+feature -- Access
+
+ blogs_count: INTEGER_64
+ -- Count of blog nodes
+ deferred
+ end
+
+ blogs_count_from_user (a_user: CMS_USER) : INTEGER_64
+ -- Number of nodes of type blog from `a_user'.
+ require
+ has_id: a_user.has_id
+ deferred
+ end
+
+ blogs: LIST [CMS_NODE]
+ -- List of nodes ordered by creation date (descending).
+ deferred
+ end
+
+ blogs_limited (limit: NATURAL_32; offset: NATURAL_32): LIST [CMS_NODE]
+ -- List of posts ordered by creation date from offset to offset + limit.
+ deferred
+ end
+
+ blogs_from_user_limited (a_user: CMS_USER; limit: NATURAL_32; offset: NATURAL_32): LIST [CMS_NODE]
+ -- List of posts from `a_user' ordered by creation date from offset to offset + limit.
+ require
+ has_id: a_user.has_id
+ deferred
+ end
+
+end
diff --git a/modules/blog/persistence/cms_blog_storage_null.e b/modules/blog/persistence/cms_blog_storage_null.e
new file mode 100644
index 0000000..40a860f
--- /dev/null
+++ b/modules/blog/persistence/cms_blog_storage_null.e
@@ -0,0 +1,47 @@
+note
+ description: "Summary description for {CMS_BLOG_STORAGE_NULL}."
+ author: ""
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_BLOG_STORAGE_NULL
+
+inherit
+ CMS_NODE_STORAGE_NULL
+
+ CMS_BLOG_STORAGE_I
+
+create
+ make
+
+feature -- Access
+
+ blogs_count: INTEGER_64
+ -- Count of nodes.
+ do
+ end
+
+ blogs_count_from_user (a_user: CMS_USER) : INTEGER_64
+ --
+ do
+ end
+
+ blogs: LIST [CMS_NODE]
+ --
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ blogs_limited (limit: NATURAL_32; offset: NATURAL_32) : LIST [CMS_NODE]
+ --
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ blogs_from_user_limited (a_user: CMS_USER; limit: NATURAL_32; offset: NATURAL_32): LIST [CMS_NODE]
+ --
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+end
diff --git a/modules/blog/persistence/cms_blog_storage_sql.e b/modules/blog/persistence/cms_blog_storage_sql.e
new file mode 100644
index 0000000..c804681
--- /dev/null
+++ b/modules/blog/persistence/cms_blog_storage_sql.e
@@ -0,0 +1,145 @@
+note
+ description: "Access to the sql database for the blog module"
+ author: "Dario Bösch "
+ date: "$Date: 2015-05-21 14:46:00 +0100$"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_BLOG_STORAGE_SQL
+
+inherit
+ CMS_NODE_STORAGE_SQL
+
+ CMS_BLOG_STORAGE_I
+
+create
+ make
+
+feature -- Access
+
+ blogs_count: INTEGER_64
+ --
+ do
+ error_handler.reset
+ write_information_log (generator + ".blogs_count")
+ sql_query (sql_select_blog_count, Void)
+ if not has_error and not sql_after then
+ Result := sql_read_integer_64 (1)
+ end
+ sql_finalize
+ end
+
+ blogs_count_from_user (a_user: CMS_USER) : INTEGER_64
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".blogs_count_from_user")
+ create l_parameters.make (2)
+ l_parameters.put (a_user.id, "user")
+ sql_query (sql_select_blog_count_from_user, l_parameters)
+ if not has_error and not sql_after then
+ Result := sql_read_integer_64 (1)
+ end
+ sql_finalize
+ end
+
+ blogs: LIST [CMS_NODE]
+ --
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".blogs")
+
+ from
+ sql_query (sql_select_blogs_order_created_desc, Void)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ blogs_limited (a_limit: NATURAL_32; a_offset: NATURAL_32): LIST [CMS_NODE]
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".blogs_limited")
+
+ from
+ create l_parameters.make (2)
+ l_parameters.put (a_limit, "limit")
+ l_parameters.put (a_offset, "offset")
+ sql_query (sql_blogs_limited, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ blogs_from_user_limited (a_user: CMS_USER; a_limit: NATURAL_32; a_offset: NATURAL_32): LIST [CMS_NODE]
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".blogs_from_user_limited")
+
+ from
+ create l_parameters.make (2)
+ l_parameters.put (a_limit, "limit")
+ l_parameters.put (a_offset, "offset")
+ l_parameters.put (a_user.id, "user")
+ sql_query (sql_blogs_from_user_limited, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+feature {NONE} -- Queries
+
+ sql_select_blog_count: STRING = "SELECT count(*) FROM nodes WHERE status != -1 AND type = %"blog%";"
+ -- Nodes count (Published and not Published)
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_blog_count_from_user: STRING = "SELECT count(*) FROM nodes WHERE status != -1 AND type = %"blog%" AND author = :user ;"
+ -- Nodes count (Published and not Published)
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_blogs_order_created_desc: STRING = "SELECT * FROM nodes WHERE status != -1 AND type = %"blog%" ORDER BY created DESC;"
+ -- SQL Query to retrieve all nodes that are from the type "blog" ordered by descending creation date.
+
+ sql_blogs_limited: STRING = "SELECT * FROM nodes WHERE status != -1 AND type = %"blog%" ORDER BY created DESC LIMIT :limit OFFSET :offset ;"
+ --- SQL Query to retrieve all node of type "blog" limited by limit and starting at offset
+
+ sql_blogs_from_user_limited: STRING = "SELECT * FROM nodes WHERE status != -1 AND type = %"blog%" AND author = :user ORDER BY created DESC LIMIT :limit OFFSET :offset ;"
+ --- SQL Query to retrieve all node of type "blog" from author with id limited by limit + offset
+
+
+end
diff --git a/modules/blog/site/files/css/blog.css b/modules/blog/site/files/css/blog.css
new file mode 100644
index 0000000..736608d
--- /dev/null
+++ b/modules/blog/site/files/css/blog.css
@@ -0,0 +1,25 @@
+ul.cms_blog_nodes {
+ padding: 0;
+ margin: 0;
+}
+ul.cms_blog_nodes li.cms_type_blog {
+ list-style: none;
+ display: block;
+ margin-top: 20px;
+ padding-bottom: 20px;
+ border-bottom: 1px dotted black;
+}
+ul.cms_blog_nodes li.cms_type_blog .blog_title a {
+ color: black;
+ font-size: 18px;
+ text-decoration: none;
+ display: block;
+ margin: 6px 0;
+}
+ul.cms_blog_nodes li.cms_type_blog .blog_title a:hover {
+ color: #999;
+}
+ul.cms_blog_nodes li.cms_type_blog .blog_list_summary a {
+ margin-top: 20px;
+ display: block;
+}
diff --git a/modules/blog/site/files/scss/blog.scss b/modules/blog/site/files/scss/blog.scss
new file mode 100644
index 0000000..be90622
--- /dev/null
+++ b/modules/blog/site/files/scss/blog.scss
@@ -0,0 +1,30 @@
+ul.cms_blog_nodes{
+
+ padding:0;
+ margin:0;
+
+ li.cms_type_blog{
+ list-style: none;
+ display: block;
+ margin-top:20px;
+ padding-bottom:20px;
+ border-bottom:1px dotted black;
+
+ .blog_title a{
+ color:black;
+ font-size:18px;
+ text-decoration: none;
+ display:block;
+ margin:6px 0;
+
+ &:hover{
+ color:#999;
+ }
+ }
+
+ .blog_list_summary a{
+ margin-top:20px;
+ display:block;
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/feed_aggregator/feed_aggregation.e b/modules/feed_aggregator/feed_aggregation.e
new file mode 100644
index 0000000..52500c6
--- /dev/null
+++ b/modules/feed_aggregator/feed_aggregation.e
@@ -0,0 +1,128 @@
+note
+ description: "Feed aggregation parameters."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ FEED_AGGREGATION
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_name: READABLE_STRING_GENERAL)
+ do
+ create name.make_from_string_general (a_name)
+ create {ARRAYED_LIST [READABLE_STRING_8]} locations.make (0)
+ expiration := 60*60
+ description_enabled := True
+ size := 10
+ end
+
+feature -- Access
+
+ name: IMMUTABLE_STRING_32
+ -- Associated name.
+
+ expiration: INTEGER
+ -- Suggested expiration time in seconds (default: 1 hour).
+ -- If negative then never expires.
+
+ size: INTEGER
+ -- Number of entries to display per page.
+
+ description: detachable IMMUTABLE_STRING_32
+ -- Optional description.
+
+ locations: LIST [READABLE_STRING_8]
+ -- List of feed location aggregated into current.
+
+ included_categories: detachable LIST [READABLE_STRING_32]
+ -- Optional categories to filter.
+ -- If Void, include any.
+
+ description_enabled: BOOLEAN
+ -- Display description?
+
+feature -- Status report
+
+ has_category_filter: BOOLEAN
+ -- Is there any category filtering?
+ -- i.e via `included_categories'
+ do
+ Result := attached included_categories as cats and then not cats.is_empty
+ end
+
+feature -- Element change
+
+ set_description (a_desc: detachable READABLE_STRING_GENERAL)
+ do
+ if a_desc = Void then
+ description := Void
+ else
+ create description.make_from_string_general (a_desc)
+ end
+ end
+
+ set_expiration (nb_seconds: INTEGER)
+ -- Set `expiration' to `nb_seconds'.
+ do
+ expiration := nb_seconds
+ end
+
+ set_size (nb: INTEGER)
+ -- Set `size' to `nb'.
+ do
+ size := nb
+ end
+
+ set_description_enabled (b: BOOLEAN)
+ -- Set `description_enabled' to `b'.
+ do
+ description_enabled := b
+ end
+
+ reset_categories
+ do
+ included_categories := Void
+ end
+
+ include_category (a_cat: READABLE_STRING_GENERAL)
+ local
+ lst: like included_categories
+ s32: STRING_32
+ do
+ lst := included_categories
+ if lst = Void then
+ create {ARRAYED_LIST [READABLE_STRING_32]} lst.make (1)
+ included_categories := lst
+ lst.compare_objects
+ end
+ s32 := a_cat.to_string_32
+ if not lst.has (s32) then
+ lst.force (s32)
+ end
+ end
+
+feature -- Status report
+
+ is_included (e: FEED_ITEM): BOOLEAN
+ -- Is `e' included in final aggregation?
+ -- i.e: related to `included_categories'
+ -- note that if `e' has no category, it is included by default,
+ -- even if `included_categories' is defined.
+ do
+ Result := True
+ if attached e.categories as e_cats then
+ if attached included_categories as lst then
+ Result := across lst as ic some
+ across e_cats as e_ic some
+ e_ic.item.same_string (ic.item)
+ end
+ end
+ end
+ end
+ end
+
+end
diff --git a/modules/feed_aggregator/feed_aggregator-safe.ecf b/modules/feed_aggregator/feed_aggregator-safe.ecf
new file mode 100644
index 0000000..c43b3e8
--- /dev/null
+++ b/modules/feed_aggregator/feed_aggregator-safe.ecf
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/feed_aggregator/feed_aggregator_api.e b/modules/feed_aggregator/feed_aggregator_api.e
new file mode 100644
index 0000000..a5166c4
--- /dev/null
+++ b/modules/feed_aggregator/feed_aggregator_api.e
@@ -0,0 +1,153 @@
+note
+ description: "API for Feed aggregator module."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ FEED_AGGREGATOR_API
+
+inherit
+ CMS_MODULE_API
+
+create
+ make
+
+feature -- Access
+
+ aggregations: HASH_TABLE [FEED_AGGREGATION, STRING]
+ -- List of feed aggregations.
+ local
+ agg: FEED_AGGREGATION
+ l_feed_id: READABLE_STRING_32
+ l_title: detachable READABLE_STRING_GENERAL
+ l_locations: detachable STRING_TABLE [READABLE_STRING_8]
+ utf: UTF_CONVERTER
+ l_table: like internal_aggregations
+ do
+ l_table := internal_aggregations
+ if l_table /= Void then
+ Result := l_table
+ else
+ create Result.make (0)
+ internal_aggregations := Result
+ if attached cms_api.module_configuration_by_name ({FEED_AGGREGATOR_MODULE}.name, "feeds") as cfg then
+ if attached cfg.text_list_item ("ids") as l_ids then
+ across
+ l_ids as ic
+ loop
+ l_feed_id := ic.item
+ create l_locations.make (1)
+
+ if attached cfg.text_list_item ({STRING_32} "feeds." + l_feed_id + ".locations") as l_location_list then
+ across
+ l_location_list as loc_ic
+ loop
+ l_locations.force (utf.utf_32_string_to_utf_8_string_8 (loc_ic.item), loc_ic.item)
+ end
+ end
+ if attached cfg.text_table_item ({STRING_32} "feeds." + l_feed_id + ".locations") as l_location_table then
+ across
+ l_location_table as loc_tb_ic
+ loop
+ l_locations.force (utf.utf_32_string_to_utf_8_string_8 (loc_tb_ic.item), loc_tb_ic.key)
+ end
+ end
+ if
+ attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".location") as l_location
+ then
+ l_locations.force (utf.utf_32_string_to_utf_8_string_8 (l_location), l_location)
+ end
+ if l_locations /= Void and then not l_locations.is_empty then
+ l_title := cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".title")
+ if l_title = Void then
+ l_title := l_feed_id
+ end
+ create agg.make (l_title)
+ if attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".expiration") as l_expiration then
+ if l_expiration.is_integer then
+ agg.set_expiration (l_expiration.to_integer)
+ end
+ end
+ if attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".size") as l_size then
+ if l_size.is_integer then
+ agg.set_size (l_size.to_integer)
+ end
+ end
+ if attached cfg.text_item ({STRING_32} "feeds." + l_feed_id + ".option_description") as l_description_opt then
+ agg.set_description_enabled (not l_description_opt.is_case_insensitive_equal_general ("disabled"))
+ end
+ across
+ l_locations as loc_ic
+ loop
+ agg.locations.force (utf.utf_32_string_to_utf_8_string_8 (loc_ic.item))
+ end
+ Result.force (agg, l_feed_id)
+ if attached cfg.text_list_item ({STRING_32} "feeds." + l_feed_id + ".categories") as l_cats then
+ across
+ l_cats as cats_ic
+ loop
+ agg.include_category (cats_ic.item)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ aggregation (a_name: READABLE_STRING_GENERAL): detachable FEED_AGGREGATION
+ do
+ if attached a_name.is_valid_as_string_8 then
+ Result := aggregations.item (a_name.as_string_8)
+ end
+ end
+
+feature {NONE} -- Access: implementation
+
+ internal_aggregations: detachable like aggregations
+ -- Cache value for `aggregations'.
+
+feature -- Operation
+
+ feed (a_location: READABLE_STRING_8): detachable FEED
+ local
+ fac: FEED_DEFAULT_PARSERS
+ ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT
+ do
+ create fac
+ if attached new_http_client_session (a_location).get ("", ctx) as res then
+ if attached res.body as l_content then
+ Result := fac.feed_from_string (l_content)
+ end
+ end
+ end
+
+ aggregation_feed (agg: FEED_AGGREGATION): detachable FEED
+ -- Feed from aggregation `agg'.
+ do
+ across
+ agg.locations as ic
+ loop
+ if attached feed (ic.item) as f then
+ if Result /= Void then
+ if f /= Void then
+ Result := Result + f
+ end
+ else
+ Result := f
+ end
+ end
+ end
+ end
+
+ new_http_client_session (a_url: READABLE_STRING_8): HTTP_CLIENT_SESSION
+ local
+ cl: LIBCURL_HTTP_CLIENT
+ do
+ create cl.make
+ Result := cl.new_session (a_url)
+ Result.set_is_insecure (True)
+ end
+
+end
diff --git a/modules/feed_aggregator/feed_aggregator_module.e b/modules/feed_aggregator/feed_aggregator_module.e
new file mode 100644
index 0000000..6c64df5
--- /dev/null
+++ b/modules/feed_aggregator/feed_aggregator_module.e
@@ -0,0 +1,355 @@
+note
+ description: "CMS module bringing support for feed aggregation."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ FEED_AGGREGATOR_MODULE
+
+inherit
+ CMS_MODULE
+ rename
+ module_api as feed_aggregator_api
+ redefine
+ initialize,
+ setup_hooks,
+ permissions,
+ feed_aggregator_api
+ end
+
+ CMS_HOOK_BLOCK
+
+ CMS_HOOK_RESPONSE_ALTER
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_CACHE
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create Current module, disabled by default.
+ do
+ version := "1.0"
+ description := "Feed aggregation"
+ package := "feed"
+ end
+
+feature -- Access
+
+ name: STRING = "feed_aggregator"
+
+ permissions: LIST [READABLE_STRING_8]
+ -- List of permission ids, used by this module, and declared.
+ do
+ Result := Precursor
+ Result.force (permission__manage_feed_aggregator)
+ Result.force (permission__clear_feed_cache)
+ end
+
+ permission__manage_feed_aggregator: STRING = "manage feed aggregator"
+ permission__clear_feed_cache: STRING = "clear feed cache"
+
+feature {CMS_API} -- Module Initialization
+
+ initialize (api: CMS_API)
+ --
+ do
+ Precursor (api)
+ create feed_aggregator_api.make (api)
+ end
+
+feature {CMS_API} -- Access: API
+
+ feed_aggregator_api: detachable FEED_AGGREGATOR_API
+ -- Eventual module api.
+
+feature -- Access: router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ local
+ h: WSF_URI_TEMPLATE_HANDLER
+ do
+ a_router.handle ("/admin/feed_aggregator/", create {WSF_URI_AGENT_HANDLER}.make (agent handle_feed_aggregator_admin (a_api, ?, ?)), a_router.methods_head_get_post)
+ create {WSF_URI_TEMPLATE_AGENT_HANDLER} h.make (agent handle_feed_aggregation (a_api, ?, ?))
+ a_router.handle ("/feed_aggregation/", h, a_router.methods_head_get)
+ a_router.handle ("/feed_aggregation/{feed_id}", h, a_router.methods_head_get)
+ end
+
+feature -- Handle
+
+ handle_feed_aggregator_admin (a_api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ nyi: NOT_IMPLEMENTED_ERROR_CMS_RESPONSE
+ do
+ create nyi.make (req, res, a_api)
+ nyi.execute
+ end
+
+ handle_feed_aggregation (a_api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ s: STRING
+ nb: INTEGER
+ do
+ if attached {WSF_STRING} req.query_parameter ("size") as p_size and then p_size.is_integer then
+ nb := p_size.integer_value
+ else
+ nb := -1
+ end
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, a_api)
+ if attached {WSF_STRING} req.path_parameter ("feed_id") as p_feed_id then
+ if attached feed_aggregation (p_feed_id.value) as l_agg then
+ create s.make_empty
+ s.append ("")
+ s.append (r.html_encoded (l_agg.name))
+ s.append (" ")
+ if attached l_agg.included_categories as l_categories then
+ s.append ("")
+ across
+ l_categories as cats_ic
+ loop
+ s.append (" [")
+ s.append (r.html_encoded (cats_ic.item))
+ s.append ("]")
+ end
+ s.append (" ")
+ end
+ if attached l_agg.description as l_desc and then l_desc.is_valid_as_string_8 then
+ s.append ("")
+ s.append (l_desc.as_string_8)
+ s.append ("
")
+ end
+ s.append ("")
+
+ if attached feed_to_html (p_feed_id.value, nb, True, r) as l_html then
+ s.append (l_html)
+ end
+ r.set_main_content (s)
+ else
+ create {NOT_FOUND_ERROR_CMS_RESPONSE} r.make (req, res, a_api)
+ end
+ else
+ if attached feed_aggregator_api as l_feed_agg_api then
+ create s.make_empty
+ across
+ l_feed_agg_api.aggregations as ic
+ loop
+ s.append ("")
+ s.append (r.link (ic.key, "feed_aggregation/" + r.url_encoded (ic.key), Void))
+ if attached ic.item.included_categories as l_categories then
+ s.append ("")
+ across
+ l_categories as cats_ic
+ loop
+ s.append (" [")
+ s.append (r.html_encoded (cats_ic.item))
+ s.append ("]")
+ end
+ s.append (" ")
+ end
+ if attached ic.item.description as l_desc then
+ if l_desc.is_valid_as_string_8 then
+ s.append ("")
+ s.append (l_desc.as_string_8)
+ s.append ("
")
+ end
+ end
+ s.append (" ")
+ end
+ r.set_main_content (s)
+ else
+ create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, a_api)
+ end
+ end
+ r.execute
+ end
+
+feature -- Hooks configuration
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ -- Module hooks configuration.
+ do
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_response_alter_hook (Current)
+ a_hooks.subscribe_to_menu_system_alter_hook (Current)
+ a_hooks.subscribe_to_cache_hook (Current)
+ end
+
+feature -- Hook
+
+ clear_cache (a_cache_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_response: CMS_RESPONSE)
+ -- .
+ local
+ p: PATH
+ dir: DIRECTORY
+ do
+ if a_response.has_permissions (<>) then
+ if a_cache_id_list = Void then
+ -- Clear all cache.
+ p := a_response.api.files_location.extended (".cache").extended (name)
+ create dir.make_with_path (p)
+ if dir.exists then
+ dir.recursive_delete
+ end
+ end
+ end
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ -- List of block names, managed by current object.
+ local
+ res: ARRAYED_LIST [like {CMS_BLOCK}.name]
+ l_aggs: HASH_TABLE [FEED_AGGREGATION, STRING_8]
+ do
+ if attached feed_aggregator_api as l_feed_api then
+ l_aggs := l_feed_api.aggregations
+ create res.make (l_aggs.count)
+ across
+ l_aggs as ic
+ loop
+ res.force ("?feed." + ic.key)
+ end
+ else
+ create res.make (0)
+ end
+ Result := res
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ -- Get block object identified by `a_block_id' and associate with `a_response'.
+ local
+ s: READABLE_STRING_8
+ b: CMS_CONTENT_BLOCK
+ pref: STRING
+ nb: INTEGER
+ do
+ if attached feed_aggregator_api as l_feed_api then
+ pref := "feed."
+ if a_block_id.starts_with (pref) then
+ s := a_block_id.substring (pref.count + 1, a_block_id.count)
+ else
+ s := a_block_id
+ end
+ nb := 0
+ if
+ attached a_response.block_options (a_block_id) as l_options and then
+ attached {READABLE_STRING_GENERAL} l_options.item ("size") as l_size and then
+ l_size.is_integer
+ then
+ nb := l_size.to_integer
+ end
+ if attached feed_to_html (s, nb, True, a_response) as l_content then
+ create b.make (a_block_id, Void, l_content, Void)
+ b.set_is_raw (True)
+ a_response.add_block (b, "feed_" + s)
+ end
+ end
+ end
+
+ feed_aggregation (a_feed_id: READABLE_STRING_GENERAL): detachable FEED_AGGREGATION
+ do
+ if attached feed_aggregator_api as l_feed_api then
+ Result := l_feed_api.aggregation (a_feed_id)
+ end
+ end
+
+ feed_to_html (a_feed_id: READABLE_STRING_GENERAL; a_count: INTEGER; with_feed_info: BOOLEAN; a_response: CMS_RESPONSE): detachable STRING
+ local
+ nb: INTEGER
+ i: INTEGER
+ e: FEED_ITEM
+ l_cache: CMS_FILE_STRING_8_CACHE
+ lnk: detachable FEED_LINK
+ vis: FEED_TO_XHTML_VISITOR
+ s: STRING
+ do
+ if attached feed_aggregator_api as l_feed_api then
+ if attached l_feed_api.aggregation (a_feed_id) as l_agg then
+ create l_cache.make (a_response.api.files_location.extended (".cache").extended (name).extended ("feed__" + a_feed_id + "__" + a_count.out + "_" + with_feed_info.out))
+ Result := l_cache.item
+ if Result = Void or l_cache.expired (Void, l_agg.expiration) then
+
+ create Result.make (1024)
+ Result.append ("")
+
+ create vis.make (Result)
+ if a_count = 0 then
+ nb := l_agg.size
+ else
+ nb := a_count
+ end
+ vis.set_limit (nb)
+ vis.set_description_enabled (l_agg.description_enabled)
+
+ if with_feed_info then
+ create s.make_empty
+ if attached l_agg.description as l_desc then
+ s.append ("")
+ s.append_string_general (l_desc)
+ s.append ("
")
+ end
+ vis.set_header (s)
+ end
+ create s.make_empty
+ s.append_string ("")
+ s.append_string (a_response.link ("See more ...", "feed_aggregation/" + a_response.url_encoded (a_feed_id), Void))
+ s.append_string ("")
+ vis.set_footer (s)
+
+ if attached l_feed_api.aggregation_feed (l_agg) as l_feed then
+ if l_agg.has_category_filter and attached l_feed.items as lst then
+ from
+ lst.start
+ until
+ lst.after
+ loop
+ if not l_agg.is_included (lst.item_for_iteration) then
+ lst.remove
+ else
+ lst.forth
+ end
+ end
+ end
+ l_feed.accept (vis)
+ end
+ l_cache.put (Result)
+ end
+ end
+ end
+ end
+
+feature -- Hook
+
+ response_alter (a_response: CMS_RESPONSE)
+ do
+ a_response.add_style (a_response.url ("/module/" + name + "/files/css/feed_aggregator.css", Void), Void)
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ -- Hook execution on collection of menu contained by `a_menu_system'
+ -- for related response `a_response'.
+ do
+ a_menu_system.navigation_menu.extend (create {CMS_LOCAL_LINK}.make ("Feeds", "feed_aggregation/"))
+ if a_response.has_permission (permission__manage_feed_aggregator) then
+ a_menu_system.management_menu.extend (create {CMS_LOCAL_LINK}.make ("Feeds (admin)", "admin/feed_aggregator/"))
+ end
+ end
+
+end
diff --git a/modules/feed_aggregator/site/config/feeds.json.example b/modules/feed_aggregator/site/config/feeds.json.example
new file mode 100644
index 0000000..b147004
--- /dev/null
+++ b/modules/feed_aggregator/site/config/feeds.json.example
@@ -0,0 +1,28 @@
+{
+ "ids": ["news", "forum"],
+ "feeds": {
+ "news": {
+ "title": "Eiffel related posts",
+ "expiration": "21600",
+ "size": 5,
+ "locations": [
+ "https://bertrandmeyer.com/feed/",
+ "https://room.eiffel.com/blog/feed",
+ "https://room.eiffel.com/article/feed",
+ "https://room.eiffel.com/library/feed"
+ ]
+ , "categories": ["Eiffel"]
+ ,"option_description": "enabled"
+ },
+ "forum": {
+ "title": "Eiffel Forum",
+ "expiration": "21600",
+ "size": 5,
+ "locations": [
+ "https://groups.google.com/forum/feed/eiffel-users/msgs/atom.xml?num=15",
+ "http://stackoverflow.com/feeds/tag?tagnames=eiffel&sort=newest"
+ ]
+ ,"option_description": "enabled"
+ }
+ }
+}
diff --git a/modules/feed_aggregator/site/files/css/feed_aggregator.css b/modules/feed_aggregator/site/files/css/feed_aggregator.css
new file mode 100644
index 0000000..d0e322e
--- /dev/null
+++ b/modules/feed_aggregator/site/files/css/feed_aggregator.css
@@ -0,0 +1,53 @@
+div.feed ul {
+ list-style: none;
+ position: relative;
+ padding: 0;
+ margin: 0;
+ width: 99%;
+}
+div.feed li {
+ /* border-top: solid 1px #ddd; */
+ padding: 0;
+ margin: 0 0 5px 0;
+}
+div.feed li a {
+ font-weight: bold;
+}
+div.feed li .date {
+ font-weight: bold;
+ font-size: small;
+}
+div.feed li .category {
+ margin-left: 20px;
+ font-size: 8px;
+ height: 9px;
+ overflow: hidden;
+ color: #999;
+}
+div.feed li .description {
+ margin-left: 20px;
+ font-size: small;
+ height: 18px;
+ overflow: hidden;
+ color: #999;
+}
+div.feed li:hover {
+ margin-bottom: 23px;
+}
+div.feed li:hover .description {
+ padding: 5px;
+ position: absolute;
+ height: auto;
+ overflow-y: scroll;
+ overflow-x: scroll;
+ color: #000;
+ background-color: #fff;
+ border: solid 1px #000;
+ z-index: 10;
+}
+div.feed li:hover:last-child {
+ margin-bottom: 28px;
+}
+div.feed li .description::after {
+ content: "...";
+}
diff --git a/modules/feed_aggregator/site/files/scss/feed_aggregator.scss b/modules/feed_aggregator/site/files/scss/feed_aggregator.scss
new file mode 100644
index 0000000..82c2b81
--- /dev/null
+++ b/modules/feed_aggregator/site/files/scss/feed_aggregator.scss
@@ -0,0 +1,54 @@
+div.feed {
+ ul {
+ list-style: none;
+ position: relative;
+ padding: 0;
+ margin: 0;
+ width: 99%;
+ }
+ li {
+ /* border-top: solid 1px #ddd; */
+ padding: 0;
+ margin: 0 0 5px 0;
+
+ a {
+ font-weight: bold;
+ }
+ .date {
+ font-weight: bold;
+ font-size: small;
+ }
+ .category {
+ margin-left: 20px;
+ font-size: 8px;
+ height: 9px;
+ overflow: hidden;
+ color: #999;
+ }
+ .description {
+ margin-left: 20px;
+ font-size: small;
+ height: 18px;
+ overflow: hidden;
+ color: #999;
+ }
+ &:hover {
+ margin-bottom: 23px;
+ .description {
+ padding: 5px;
+ position: absolute;
+ height: auto;
+ overflow-y: scroll;
+ overflow-x: scroll;
+ color: #000;
+ background-color: #fff;
+ border: solid 1px #000;
+ z-index: 10;
+ }
+ &:last-child {
+ margin-bottom: 28px;
+ }
+ }
+ .description::after { content: "..."; }
+ }
+}
diff --git a/modules/google_search/Readme.md b/modules/google_search/Readme.md
new file mode 100644
index 0000000..279cd07
--- /dev/null
+++ b/modules/google_search/Readme.md
@@ -0,0 +1 @@
+Google Custom Search Module.
\ No newline at end of file
diff --git a/modules/google_search/google_search-safe.ecf b/modules/google_search/google_search-safe.ecf
new file mode 100644
index 0000000..965dd78
--- /dev/null
+++ b/modules/google_search/google_search-safe.ecf
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/google_search/google_search.ecf b/modules/google_search/google_search.ecf
new file mode 100644
index 0000000..ef6b56f
--- /dev/null
+++ b/modules/google_search/google_search.ecf
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/google_search/site/config/google_search.json b/modules/google_search/site/config/google_search.json
new file mode 100644
index 0000000..9b7ab8c
--- /dev/null
+++ b/modules/google_search/site/config/google_search.json
@@ -0,0 +1,6 @@
+{
+ "gcse": {
+ "cx":"",
+ "secret_key":""
+ }
+}
diff --git a/modules/google_search/site/templates/block_search.tpl b/modules/google_search/site/templates/block_search.tpl
new file mode 100644
index 0000000..b11c31b
--- /dev/null
+++ b/modules/google_search/site/templates/block_search.tpl
@@ -0,0 +1,40 @@
+
+
+ Results for {$result.current_page.search_terms/}
+
+
+
+
+
+
+ {foreach from="$result.items" item="item"}
+
+
+
+
+ {$item.html_snippet/}
+
+
+
+
+ {/foreach}
+
+
+
+
+ {if isset="$result.previous_page"}
+ Previous
+ {/if}
+ {if isset="$result.next_page"}
+ Next
+ {/if}
+
+
diff --git a/modules/google_search/src/google_custom_search_module.e b/modules/google_search/src/google_custom_search_module.e
new file mode 100644
index 0000000..d325eb0
--- /dev/null
+++ b/modules/google_search/src/google_custom_search_module.e
@@ -0,0 +1,150 @@
+note
+ description: "[
+ Module providing Google Custom Search functionality.
+ ]"
+ date: "$Date: 2015-10-09 20:50:01 -0300 (vi. 09 de oct. de 2015) $"
+ revision: "$Revision: 97982 $"
+
+class
+ GOOGLE_CUSTOM_SEARCH_MODULE
+
+inherit
+
+ CMS_MODULE
+
+ CMS_HOOK_BLOCK_HELPER
+
+ SHARED_EXECUTION_ENVIRONMENT
+ export
+ {NONE} all
+ end
+
+ REFACTORING_HELPER
+
+ SHARED_LOGGER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create current module
+ do
+ version := "1.0"
+ description := "Google custome search module"
+ package := "search"
+ end
+
+feature -- Access
+
+ name: STRING = "google_search"
+ --
+
+feature -- Router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ -- Router configuration.
+ do
+ a_router.handle ("/gcse", create {WSF_URI_AGENT_HANDLER}.make (agent handle_search (a_api, ?, ?)), a_router.methods_head_get)
+ end
+
+feature -- Recaptcha
+
+ gcse_secret_key (api: CMS_API): detachable READABLE_STRING_8
+ -- Get recaptcha security key.
+ local
+ utf: UTF_CONVERTER
+ do
+ if attached api.module_configuration (Current, Void) as cfg then
+ if
+ attached cfg.text_item ("gcse.secret_key") as l_recaptcha_key and then
+ not l_recaptcha_key.is_empty
+ then
+ Result := utf.utf_32_string_to_utf_8_string_8 (l_recaptcha_key)
+ end
+ end
+ end
+
+ gcse_cx_key (api: CMS_API): detachable READABLE_STRING_8
+ -- Get recaptcha security key.
+ local
+ utf: UTF_CONVERTER
+ do
+ if attached api.module_configuration (Current, Void) as cfg then
+ if
+ attached cfg.text_item ("gcse.cx") as l_recaptcha_key and then
+ not l_recaptcha_key.is_empty
+ then
+ Result := utf.utf_32_string_to_utf_8_string_8 (l_recaptcha_key)
+ end
+ end
+ end
+
+feature -- Handler
+
+ handle_search (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_parameters:GCSE_QUERY_PARAMETERS
+ l_search: GCSE_API
+ do
+ -- TODO handle errors!!!
+ write_debug_log (generator + ".handle_search")
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if
+ attached {WSF_STRING} req.query_parameter ("q") as l_query and then
+ not l_query.value.is_empty
+ then
+ if
+ attached gcse_cx_key (api) as l_cx and then
+ attached gcse_secret_key (api) as l_key
+ then
+ create l_parameters.make (l_key, l_cx, l_query.url_encoded_value )
+ if
+ attached {WSF_STRING} req.query_parameter ("start") as l_index and then
+ attached {WSF_STRING} req.query_parameter ("num") as l_num
+ then
+ l_parameters.set_start (l_index.value)
+ l_parameters.set_num (l_num.value)
+ end
+ create l_search.make (l_parameters)
+ l_search.search
+ if
+ attached l_search.last_result as l_result and then
+ l_result.status = 200
+ then
+ if attached template_block (Current, "search", r) as l_tpl_block then
+ l_tpl_block.set_value (l_result, "result")
+ r.add_block (l_tpl_block, "content")
+ end
+ else
+ -- Quota limit (403 status code) or not results.
+ google_search_site (req, r, l_query)
+ end
+ else
+ -- If no key are provided, at least output google search result page.
+ google_search_site (req, r, l_query)
+ end
+ else
+ r.add_message ("No query submitted", Void)
+ end
+ r.execute
+ end
+
+feature {NONE} -- Helper
+
+ google_search_site (req: WSF_REQUEST; res: CMS_RESPONSE; query: WSF_STRING)
+ -- Workaround to output google search result page
+ -- If no key are provided or if GCSE reached the quota limit.
+ local
+ l_url_encoder: URL_ENCODER
+ do
+ create l_url_encoder
+ if req.is_https then
+ res.set_redirection ("https://www.google.com/search?sitesearch=" + l_url_encoder.general_encoded_string (res.absolute_url ("", Void)) + "&q=" + query.url_encoded_value)
+ else
+ res.set_redirection ("http://www.google.com/search?sitesearch=" + l_url_encoder.general_encoded_string (res.absolute_url ("", Void)) + "&q=" + query.url_encoded_value)
+ end
+ end
+end
diff --git a/modules/node/cms_node_api.e b/modules/node/cms_node_api.e
new file mode 100644
index 0000000..bc4523e
--- /dev/null
+++ b/modules/node/cms_node_api.e
@@ -0,0 +1,431 @@
+note
+ description: "[
+ API to manage CMS Nodes
+ ]"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_NODE_API
+
+inherit
+ CMS_MODULE_API
+ redefine
+ initialize
+ end
+
+ REFACTORING_HELPER
+
+create {CMS_NODE_MODULE}
+ make_with_storage
+
+feature {NONE} -- Initialization
+
+ make_with_storage (a_api: CMS_API; a_node_storage: CMS_NODE_STORAGE_I)
+ do
+ node_storage := a_node_storage
+ make (a_api)
+-- error_handler.add_synchronization (a_node_storage.error_handler)
+ end
+
+ initialize
+ --
+ do
+ Precursor
+ initialize_node_types
+ end
+
+ initialize_node_types
+ -- Initialize content type system.
+ local
+ ct: CMS_PAGE_NODE_TYPE
+ do
+ -- Initialize node content types.
+ create ct
+ --| For now, add all available formats to content type `ct'.
+ across
+ cms_api.formats as ic
+ loop
+ ct.extend_format (ic.item)
+ end
+ add_node_type (ct)
+ add_node_type_webform_manager (create {CMS_PAGE_NODE_TYPE_WEBFORM_MANAGER}.make (ct, Current))
+ end
+
+feature {CMS_MODULE} -- Access nodes storage.
+
+ node_storage: CMS_NODE_STORAGE_I
+
+feature -- Content type
+
+ add_node_type (a_type: CMS_NODE_TYPE [CMS_NODE])
+ -- Register node content type `a_type'.
+ do
+ cms_api.add_content_type (a_type)
+ end
+
+ node_types: ARRAYED_LIST [attached like node_type]
+ -- Node content types.
+ do
+ create Result.make (cms_api.content_types.count)
+ across
+ cms_api.content_types as ic
+ loop
+ if attached {like node_type} ic.item as l_node_type then
+ Result.extend (l_node_type)
+ end
+ end
+ end
+
+ node_type (a_name: READABLE_STRING_GENERAL): detachable CMS_NODE_TYPE [CMS_NODE]
+ -- Content type named `a_named' if any.
+ do
+ across
+ cms_api.content_types as ic
+ until
+ Result /= Void
+ loop
+ if
+ attached {like node_type} ic.item as l_node_type and then
+ a_name.is_case_insensitive_equal (l_node_type.name)
+ then
+ Result := l_node_type
+ end
+ end
+ end
+
+ node_type_for (a_node: CMS_NODE): like node_type
+ -- Content type for node `a_node' if any.
+ local
+ l_type_name: READABLE_STRING_8
+ do
+ l_type_name := a_node.content_type
+ Result := node_type (l_type_name)
+ end
+
+feature -- Content type webform
+
+ content_type_webform_managers: ARRAYED_LIST [CMS_CONTENT_TYPE_WEBFORM_MANAGER [CMS_CONTENT]]
+ -- Available content types
+ do
+ Result := cms_api.content_type_webform_managers
+ end
+
+ add_node_type_webform_manager (a_manager: CMS_NODE_TYPE_WEBFORM_MANAGER [CMS_NODE])
+ -- Register webform manager `a_manager'.
+ do
+ cms_api.add_content_type_webform_manager (a_manager)
+ end
+
+ node_type_webform_manager (a_node_type: CMS_CONTENT_TYPE): detachable CMS_NODE_TYPE_WEBFORM_MANAGER_I [CMS_NODE]
+ -- Web form manager for node type `a_node_type' if any.
+ local
+ l_type_name: READABLE_STRING_GENERAL
+ do
+ l_type_name := a_node_type.name
+ across
+ content_type_webform_managers as ic
+ until
+ Result /= Void
+ loop
+ if
+ attached {like node_type_webform_manager} ic.item as l_manager and then
+ l_type_name.is_case_insensitive_equal (l_manager.name)
+ then
+ Result := l_manager
+ end
+ end
+ end
+
+feature -- URL
+
+ new_content_path (ct: detachable CMS_CONTENT_TYPE): STRING
+ -- URI path for new content of type `ct'
+ -- or URI of path for selection of new content possibilities if ct is Void.
+ do
+ if ct /= Void then
+ Result := "node/add/" + ct.name
+ else
+ Result := "node/"
+ end
+ end
+
+ node_link (a_node: CMS_NODE): CMS_LOCAL_LINK
+ -- CMS link for node `a_node'.
+ require
+ a_node.has_id
+ do
+ create Result.make (a_node.title, cms_api.location_alias (node_path (a_node)))
+ end
+
+ node_path (a_node: CMS_NODE): STRING
+ -- URI path for node `a_node'.
+ -- using the /node/{nid} url.
+ require
+ a_node.has_id
+ do
+ Result := "node/" + a_node.id.out
+ end
+
+ nodes_path: STRING
+ -- URI path for list of nodes.
+ do
+ Result := "nodes"
+ end
+
+feature -- Access: Node
+
+ nodes_count: NATURAL_64
+ do
+ Result := node_storage.nodes_count
+ end
+
+ nodes: LIST [CMS_NODE]
+ -- List of nodes.
+ do
+ Result := node_storage.nodes
+ end
+
+ node_revisions (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- List of revisions for node `a_node'.
+ do
+ Result := node_storage.node_revisions (a_node)
+ Result := full_nodes (Result)
+ end
+
+ trashed_nodes (a_user: detachable CMS_USER): LIST [CMS_NODE]
+ -- List of nodes with status in {CMS_NODE_API}.trashed.
+ -- if `a_user' is set, return nodes related to this user.
+ do
+ Result := node_storage.trashed_nodes (a_user)
+ end
+
+ recent_nodes (params: CMS_DATA_QUERY_PARAMETERS): ITERABLE [CMS_NODE]
+ -- List of most recent nodes according to `params.offset' and `params.size'.
+ do
+ Result := node_storage.recent_nodes (params.offset.to_integer_32, params.size.to_integer_32)
+ end
+
+ recent_node_changes_before (params: CMS_DATA_QUERY_PARAMETERS; a_date: DATE_TIME): ITERABLE [CMS_NODE]
+ -- List of recent changes, before `a_date', according to `params' settings.
+ do
+ Result := node_storage.recent_node_changes_before (params.offset.to_integer_32, params.size.to_integer_32, a_date)
+ end
+
+ node (a_id: INTEGER_64): detachable CMS_NODE
+ -- Node by ID.
+ do
+ debug ("refactor_fixme")
+ fixme ("Check preconditions")
+ end
+ Result := safe_full_node (node_storage.node_by_id (a_id))
+ end
+
+ revision_node (a_node_id: INTEGER_64; a_revision_id: INTEGER_64): detachable CMS_NODE
+ do
+ Result := safe_full_node (node_storage.node_by_id_and_revision (a_node_id, a_revision_id))
+ end
+
+ safe_full_node (a_node: detachable CMS_NODE): detachable CMS_NODE
+ do
+ if a_node /= Void then
+ Result := full_node (a_node)
+ end
+ end
+
+ full_node (a_node: CMS_NODE): CMS_NODE
+ -- If `a_node' is partial, return the full node from `a_node',
+ -- otherwise return directly `a_node'.
+ require
+ a_node_set: a_node /= Void
+ do
+ if attached {CMS_PARTIAL_NODE} a_node as l_partial_node then
+ if attached node_type_for (l_partial_node) as ct then
+ Result := ct.new_node (l_partial_node)
+ node_storage.fill_node (Result)
+ else
+ Result := l_partial_node
+ end
+ -- Update link with aliasing.
+ if Result /= Void and then Result.has_id then
+ Result.set_link (node_link (Result))
+ end
+ else
+ Result := a_node
+ if Result.has_id and Result.link = Void then
+ Result.set_link (node_link (Result))
+ end
+ end
+ check has_link: Result.has_id implies attached Result.link as lnk and then lnk.location.same_string (node_link (Result).location) end
+
+ -- Update partial user if needed.
+ if
+ Result /= Void and then
+ attached {CMS_PARTIAL_USER} Result.author as l_partial_author
+ then
+ if attached cms_api.user_api.user_by_id (l_partial_author.id) as l_author then
+ Result.set_author (l_author)
+ else
+ check
+ valid_author_id: False
+ end
+ end
+ end
+ end
+
+ full_nodes (a_nodes: LIST [CMS_NODE]): LIST [CMS_NODE]
+ -- Convert list of nodes into a list of nodes when possible.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (a_nodes.count)
+
+ across
+ a_nodes as ic
+ loop
+ if attached full_node (ic.item) as l_full then
+ Result.force (l_full)
+ end
+ end
+ end
+
+ is_author_of_node (u: CMS_USER; a_node: CMS_NODE): BOOLEAN
+ -- Is the user `u' owner of the node `n'.
+ do
+ if attached node_storage.node_author (a_node) as l_author then
+ Result := u.same_as (l_author)
+ end
+ end
+
+ nodes_of_type (a_node_type: CMS_CONTENT_TYPE): LIST [CMS_NODE]
+ -- List of nodes of type `a_node_type'.
+ do
+ Result := node_storage.nodes_of_type (a_node_type)
+ ensure
+ expected_type: across Result as ic all ic.item.content_type.same_string (a_node_type.name) end
+ end
+
+feature -- Access: page/book outline
+
+ children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
+ -- Children of node `a_node'.
+ -- note: this is the partial version of the nodes.
+ do
+ Result := node_storage.children (a_node)
+ end
+
+ available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- Potential parent nodes for node `a_node'.
+ -- Ensure no cycle exists.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ across node_storage.available_parents_for_node (a_node) as ic loop
+ check distinct: not a_node.same_node (ic.item) end
+ if not is_node_a_parent_of (a_node, ic.item) then
+ Result.force (ic.item)
+ end
+ end
+ ensure
+ no_cycle: across Result as c all not is_node_a_parent_of (a_node, c.item) end
+ end
+
+ is_node_a_parent_of (a_node: CMS_NODE; a_child: CMS_NODE): BOOLEAN
+ -- Is `a_node' a direct or indirect parent of node `a_child'?
+ require
+ distinct_nodes: not a_node.same_node (a_child)
+ do
+ if
+ attached {CMS_PAGE} full_node (a_child) as l_child_page and then
+ attached l_child_page.parent as l_parent
+ then
+ if l_parent.same_node (a_node) then
+ Result := True
+ else
+ Result := is_node_a_parent_of (a_node, l_parent)
+ end
+ end
+ end
+
+feature -- Permission Scope: Node
+
+ has_permission_for_action_on_node (a_action: READABLE_STRING_8; a_node: CMS_NODE; a_user: detachable CMS_USER; ): BOOLEAN
+ -- Has permission to execute action `a_action' on node `a_node', by eventual user `a_user'?
+ local
+ l_type_name: READABLE_STRING_8
+ do
+ l_type_name := a_node.content_type
+ Result := cms_api.user_has_permission (a_user, a_action + " any " + l_type_name)
+ if not Result and a_user /= Void then
+ if is_author_of_node (a_user, a_node) then
+ Result := cms_api.user_has_permission (a_user, a_action + " own " + l_type_name)
+ end
+ end
+ end
+
+feature -- Change: Node
+
+ save_node (a_node: CMS_NODE)
+ -- Save `a_node'.
+ do
+ reset_error
+ node_storage.save_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+
+ new_node (a_node: CMS_NODE)
+ -- Add a new node `a_node'
+ require
+ no_id: not a_node.has_id
+ do
+ reset_error
+ node_storage.new_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+
+ delete_node (a_node: CMS_NODE)
+ -- Delete `a_node'.
+ --! remove the node from the storage.
+ do
+ reset_error
+ if a_node.has_id then
+ node_storage.delete_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+ end
+
+ update_node (a_node: CMS_NODE)
+ -- Update node `a_node' data.
+ do
+ reset_error
+ node_storage.update_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+
+ trash_node (a_node: CMS_NODE)
+ -- Trash node `a_node'.
+ -- Soft delete
+ do
+ reset_error
+ node_storage.trash_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+
+ restore_node (a_node: CMS_NODE)
+ -- Restore node `a_node'.
+ -- From {CMS_NODE_API}.trashed to {CMS_NODE_API}.not_published.
+ do
+ reset_error
+ node_storage.restore_node (a_node)
+ error_handler.append (node_storage.error_handler)
+ end
+
+feature -- Node status
+
+ Not_published: INTEGER = 0
+ -- The node is not published.
+
+ Published: INTEGER = 1
+ -- The node is published.
+
+ Trashed: INTEGER = -1
+ -- The node is trashed (soft delete), ready to be deleted/destroyed from storage.
+
+end
diff --git a/modules/node/cms_node_module.e b/modules/node/cms_node_module.e
new file mode 100644
index 0000000..117f8dd
--- /dev/null
+++ b/modules/node/cms_node_module.e
@@ -0,0 +1,505 @@
+note
+ description: "CMS module bringing support for NODE management."
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_NODE_MODULE
+
+inherit
+ CMS_MODULE
+ redefine
+ setup_hooks,
+ initialize,
+ is_installed,
+ install,
+ module_api,
+ permissions
+ end
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_RESPONSE_ALTER
+
+ CMS_HOOK_BLOCK
+
+ CMS_RECENT_CHANGES_HOOK
+
+ CMS_TAXONOMY_HOOK
+
+ CMS_HOOK_EXPORT
+
+ CMS_EXPORT_NODE_UTILITIES
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_setup: CMS_SETUP)
+ -- Create Current module, disabled by default.
+ do
+ version := "1.0"
+ description := "Service to manage content based on 'node'"
+ package := "core"
+ config := a_setup
+ -- Optional dependencies, mainly for information.
+ put_dependency ({CMS_RECENT_CHANGES_MODULE}, False)
+ put_dependency ({CMS_TAXONOMY_MODULE}, False)
+ end
+
+ config: CMS_SETUP
+ -- Node configuration.
+
+feature -- Access
+
+ name: STRING = "node"
+
+feature {CMS_API} -- Module Initialization
+
+ initialize (a_api: CMS_API)
+ --
+ local
+ p1,p2: CMS_PAGE
+ ct: CMS_PAGE_NODE_TYPE
+ l_node_api: like node_api
+ l_node_storage: CMS_NODE_STORAGE_I
+ do
+ Precursor (a_api)
+
+ -- Storage initialization
+ if attached a_api.storage.as_sql_storage as l_storage_sql then
+ create {CMS_NODE_STORAGE_SQL} l_node_storage.make (l_storage_sql)
+ else
+ -- FIXME: in case of NULL storage, should Current be disabled?
+ create {CMS_NODE_STORAGE_NULL} l_node_storage.make
+ end
+
+ -- Node API initialization
+ create l_node_api.make_with_storage (a_api, l_node_storage)
+ node_api := l_node_api
+
+ -- Add support for CMS_PAGE, which requires a storage extension to store the optional "parent" id.
+ -- For now, we only have extension based on SQL statement.
+ if attached {CMS_NODE_STORAGE_SQL} l_node_api.node_storage as l_sql_node_storage then
+ l_sql_node_storage.register_node_storage_extension (create {CMS_NODE_STORAGE_SQL_PAGE_EXTENSION}.make (l_node_api, l_sql_node_storage))
+
+ -- FIXME: the following code is mostly for test purpose/initialization, remove later
+ if l_sql_node_storage.sql_table_items_count ("page_nodes") = 0 then
+ if attached a_api.user_api.user_by_id (1) as u then
+ create ct
+ p1 := ct.new_node (Void)
+ p1.set_title ("Welcome")
+ p1.set_content ("Welcome, you are using the ROC Eiffel CMS", Void, Void) -- Use default format
+ p1.set_author (u)
+ l_sql_node_storage.save_node (p1)
+
+ p2 := ct.new_node (Void)
+ p2.set_title ("A new page example")
+ p2.set_content ("This is the content of a page", Void, Void) -- Use default format
+ p2.set_author (u)
+ p2.set_parent (p1)
+ l_sql_node_storage.save_node (p2)
+ end
+ end
+ else
+ -- FIXME: maybe provide a default solution based on file system, when no SQL storage is available.
+ -- IDEA: we could also have generic extension to node system, that handle generic addition field.
+ end
+ ensure then
+ node_api_set: node_api /= Void
+ end
+
+feature {CMS_API} -- Module management
+
+ is_installed (a_api: CMS_API): BOOLEAN
+ -- Is Current module installed?
+ do
+ Result := Precursor (a_api)
+ if Result and attached a_api.storage.as_sql_storage as l_sql_storage then
+ Result := l_sql_storage.sql_table_exists ("nodes") and
+ l_sql_storage.sql_table_exists ("page_nodes")
+ end
+ end
+
+ install (a_api: CMS_API)
+ do
+ -- Schema
+ if attached a_api.storage.as_sql_storage as l_sql_storage then
+ l_sql_storage.sql_execute_file_script (a_api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended (name).appended_with_extension ("sql")), Void)
+ end
+ Precursor {CMS_MODULE}(a_api)
+ end
+
+feature {CMS_API} -- Access: API
+
+ module_api: detachable CMS_MODULE_API
+ --
+ do
+ if attached node_api as l_api then
+ Result := l_api
+ else
+ -- Current is initialized, so node_api should be set.
+ check has_node_api: False end
+ end
+ end
+
+ node_api: detachable CMS_NODE_API
+ --
+
+feature -- Access
+
+ permissions: LIST [READABLE_STRING_8]
+ -- .
+ local
+ l_type_name: READABLE_STRING_8
+ do
+ Result := Precursor
+ Result.force ("create any node")
+ Result.force ("export any node")
+
+ if attached node_api as l_node_api then
+ across
+ l_node_api.node_types as ic
+ loop
+ l_type_name := ic.item.name
+ if not l_type_name.is_whitespace then
+ Result.force ("create " + l_type_name)
+
+ Result.force ("view any " + l_type_name)
+ Result.force ("edit any " + l_type_name)
+ Result.force ("delete any " + l_type_name)
+ Result.force ("trash any " + l_type_name)
+ Result.force ("restore any " + l_type_name)
+
+ Result.force ("view revisions any " + l_type_name)
+
+ Result.force ("view own " + l_type_name)
+ Result.force ("edit own " + l_type_name)
+ Result.force ("delete own " + l_type_name)
+ Result.force ("trash own " + l_type_name)
+ Result.force ("restore own " + l_type_name)
+
+ Result.force ("view unpublished " + l_type_name)
+
+ Result.force ("view revisions own " + l_type_name)
+
+ Result.force ("export " + l_type_name)
+ end
+ end
+ Result.force ("view trash")
+ end
+ end
+
+feature -- Access: router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ if attached node_api as l_node_api then
+ configure_web (a_api, l_node_api, a_router)
+ end
+ end
+
+ configure_web (a_api: CMS_API; a_node_api: CMS_NODE_API; a_router: WSF_ROUTER)
+ local
+ l_node_handler: NODE_HANDLER
+ l_nodes_handler: NODES_HANDLER
+ l_uri_mapping: WSF_URI_MAPPING
+ l_trash_handler: TRASH_HANDLER
+ do
+ -- TODO: for now, focused only on web interface, add REST api later. [2015-April-29]
+ create l_node_handler.make (a_api, a_node_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/node", l_node_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get_post)
+
+ a_router.handle ("/node/add/{type}", l_node_handler, a_router.methods_get_post)
+ a_router.handle ("/node/{id}/revision", l_node_handler, a_router.methods_get)
+ a_router.handle ("/node/{id}/edit", l_node_handler, a_router.methods_get_post)
+ a_router.handle ("/node/{id}/delete", l_node_handler, a_router.methods_get_post)
+ a_router.handle ("/node/{id}/trash", l_node_handler, a_router.methods_get_post)
+
+ a_router.handle ("/node/{id}", l_node_handler, a_router.methods_get)
+ -- For now: no REST API handling... a_router.methods_get_put_delete + a_router.methods_get_post)
+
+ -- Nodes
+ create l_nodes_handler.make (a_api, a_node_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/nodes", l_nodes_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get)
+
+ -- Trash
+ create l_trash_handler.make (a_api, a_node_api)
+ create l_uri_mapping.make_trailing_slash_ignored ("/trash", l_trash_handler)
+ a_router.map (l_uri_mapping, a_router.methods_get)
+
+ end
+
+feature -- Hooks
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ --
+ do
+ a_hooks.subscribe_to_menu_system_alter_hook (Current)
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_response_alter_hook (Current)
+ a_hooks.subscribe_to_export_hook (Current)
+
+ -- Module specific hook, if available.
+ a_hooks.subscribe_to_hook (Current, {CMS_RECENT_CHANGES_HOOK})
+ a_hooks.subscribe_to_hook (Current, {CMS_TAXONOMY_HOOK})
+ end
+
+ response_alter (a_response: CMS_RESPONSE)
+ --
+ do
+ a_response.add_style (a_response.url ("/module/" + name + "/files/css/node.css", Void), Void)
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ --
+ do
+ Result := <<"?node-info">>
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ local
+-- b: CMS_CONTENT_BLOCK
+ do
+-- create b.make (a_block_id, "Block::node", "This is a block test", Void)
+-- a_response.add_block (b, "sidebar_second")
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ local
+ lnk: CMS_LOCAL_LINK
+ perms: ARRAYED_LIST [READABLE_STRING_8]
+ do
+ debug
+ create lnk.make ("List of nodes", "nodes")
+ a_menu_system.navigation_menu.extend (lnk)
+ end
+ create lnk.make ("Trash", "trash")
+ a_menu_system.navigation_menu.extend (lnk)
+ lnk.set_permission_arguments (<<"view trash">>)
+
+ create lnk.make ("Create ..", "node")
+ a_menu_system.navigation_menu.extend (lnk)
+ if attached node_api as l_node_api then
+ create perms.make (2)
+ perms.force ("create any node")
+ across
+ l_node_api.node_types as ic
+ loop
+ perms.force ("create " + ic.item.name)
+ end
+ lnk.set_permission_arguments (perms)
+ end
+ end
+
+ recent_changes_sources: detachable LIST [READABLE_STRING_8]
+ --
+ local
+ lst: ARRAYED_LIST [READABLE_STRING_8]
+ do
+ if
+ attached node_api as l_node_api and then
+ attached l_node_api.node_types as l_types and then
+ not l_types.is_empty
+ then
+ create lst.make (l_types.count)
+ across
+ l_types as ic
+ loop
+ lst.force (ic.item.name)
+ end
+ Result := lst
+ end
+ end
+
+ populate_recent_changes (a_changes: CMS_RECENT_CHANGE_CONTAINER; a_current_user: detachable CMS_USER)
+ local
+ params: CMS_DATA_QUERY_PARAMETERS
+ ch: CMS_RECENT_CHANGE_ITEM
+ n: CMS_NODE
+ l_info: STRING_8
+ l_src: detachable READABLE_STRING_8
+ l_nodes: ITERABLE [CMS_NODE]
+ l_date: detachable DATE_TIME
+ do
+ create params.make (0, a_changes.limit)
+ if attached node_api as l_node_api then
+ l_src := a_changes.source
+ l_date := a_changes.date
+ if l_date = Void then
+ create l_date.make_now_utc
+ end
+ l_nodes := l_node_api.recent_node_changes_before (params, l_date)
+ across l_nodes as ic loop
+ n := ic.item
+ if l_src = Void or else l_src.is_case_insensitive_equal_general (n.content_type) then
+ if l_node_api.has_permission_for_action_on_node ("view", n, a_current_user) then
+ n := l_node_api.full_node (n)
+ create ch.make (n.content_type, create {CMS_LOCAL_LINK}.make (n.title, "node/" + n.id.out), n.modification_date)
+ if n.creation_date ~ n.modification_date then
+ l_info := "new"
+ if not n.is_published then
+ l_info.append (" (unpublished)")
+ end
+ else
+ if n.is_trashed then
+ l_info := "deleted"
+ else
+ l_info := "updated"
+ if not n.is_published then
+ l_info.append (" (unpublished)")
+ end
+ end
+ end
+ ch.set_information (l_info)
+ ch.set_author (n.author)
+ a_changes.force (ch)
+ else
+ -- Forbidden
+ -- FIXME: provide a visual indication!
+ end
+ end
+ end
+ end
+ end
+
+ populate_content_associated_with_term (t: CMS_TERM; a_contents: CMS_TAXONOMY_ENTITY_CONTAINER)
+ local
+ l_node_typenames: ARRAYED_LIST [READABLE_STRING_8]
+ nid: INTEGER_64
+ l_info_to_remove: ARRAYED_LIST [TUPLE [entity: READABLE_STRING_32; typename: detachable READABLE_STRING_32]]
+ do
+ if
+ attached node_api as l_node_api and then
+ attached l_node_api.node_types as l_node_types and then
+ not l_node_types.is_empty
+ then
+ create l_node_typenames.make (l_node_types.count)
+ across
+ l_node_types as ic
+ loop
+ l_node_typenames.force (ic.item.name)
+ end
+ create l_info_to_remove.make (0)
+ across
+ a_contents.taxonomy_info as ic
+ loop
+ if
+ attached ic.item.typename as l_typename and then
+ across l_node_typenames as t_ic some t_ic.item.same_string (l_typename) end
+ then
+ if ic.item.entity.is_integer then
+ nid := ic.item.entity.to_integer_64
+ if nid > 0 and then attached l_node_api.node (nid) as l_node then
+ if l_node.link = Void then
+ l_node.set_link (l_node_api.node_link (l_node))
+ end
+ a_contents.force (create {CMS_TAXONOMY_ENTITY}.make (l_node, l_node.modification_date))
+ l_info_to_remove.force (ic.item)
+ end
+ end
+ end
+ end
+ across
+ l_info_to_remove as ic
+ loop
+ a_contents.taxonomy_info.prune_all (ic.item)
+ end
+ end
+ end
+
+ export_to (a_export_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_export_parameters: CMS_EXPORT_PARAMETERS; a_response: CMS_RESPONSE)
+ -- Export data identified by `a_export_id_list',
+ -- or export all data if `a_export_id_list' is Void.
+ local
+ l_node_type: CMS_CONTENT_TYPE
+ n: CMS_NODE
+ p: PATH
+ d: DIRECTORY
+ f: PLAIN_TEXT_FILE
+ lst: LIST [CMS_NODE]
+ do
+ if attached node_api as l_node_api then
+ across
+ l_node_api.node_types as types_ic
+ loop
+ l_node_type := types_ic.item
+ if
+ a_response.has_permissions (<<"export any node", "export " + l_node_type.name>>) and then
+ l_node_type.name.same_string_general ("page") and then
+ ( a_export_id_list = Void
+ or else across a_export_id_list as ic some ic.item.same_string (l_node_type.name) end
+ )
+ then
+
+ -- For now, handle only page from this node module.
+ lst := l_node_api.nodes_of_type (l_node_type)
+ a_export_parameters.log ("Exporting " + lst.count.out + " nodes of type " + l_node_type.name)
+ p := a_export_parameters.location.extended ("nodes").extended (l_node_type.name)
+ create d.make_with_path (p)
+ if not d.exists then
+ d.recursive_create_dir
+ end
+ across
+ lst as ic
+ loop
+ n := l_node_api.full_node (ic.item)
+ a_export_parameters.log (l_node_type.name + " #" + n.id.out + " rev=" + n.revision.out)
+ create f.make_with_path (p.extended (n.id.out).appended_with_extension ("json"))
+ if not f.exists or else f.is_access_writable then
+ f.open_write
+ if attached {CMS_PAGE} n as l_page then
+ f.put_string (json_to_string (page_node_to_json (l_page)))
+ else
+ f.put_string (json_to_string (node_to_json (n)))
+ end
+ f.close
+ end
+ -- Revisions.
+ if attached l_node_api.node_revisions (n) as l_revisions and then l_revisions.count > 1 then
+ a_export_parameters.log (l_node_type.name + " " + l_revisions.count.out + " revisions.")
+ p := a_export_parameters.location.extended ("nodes").extended (l_node_type.name).extended (n.id.out)
+ create d.make_with_path (p)
+ if not d.exists then
+ d.recursive_create_dir
+ end
+ across
+ l_revisions as revs_ic
+ loop
+ n := revs_ic.item
+ create f.make_with_path (p.extended ("rev-" + n.revision.out).appended_with_extension ("json"))
+ if not f.exists or else f.is_access_writable then
+ f.open_write
+ if attached {CMS_PAGE} n as l_page then
+ f.put_string (json_to_string (page_node_to_json (l_page)))
+ else
+ f.put_string (json_to_string (node_to_json (n)))
+ end
+ f.close
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ page_node_to_json (a_page: CMS_PAGE): JSON_OBJECT
+ local
+ j: JSON_OBJECT
+ do
+ Result := node_to_json (a_page)
+ if attached a_page.parent as l_parent_page then
+ create j.make_empty
+ j.put_string (l_parent_page.content_type, "type")
+ j.put_integer (l_parent_page.id, "nid")
+ Result.put (j, "parent")
+ end
+ end
+
+end
diff --git a/modules/node/cms_node_type.e b/modules/node/cms_node_type.e
new file mode 100644
index 0000000..d96f055
--- /dev/null
+++ b/modules/node/cms_node_type.e
@@ -0,0 +1,46 @@
+note
+ description: "[
+ Interface defining a CMS content type.
+ ]"
+ status: "draft"
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_NODE_TYPE [G -> CMS_NODE]
+
+inherit
+ CMS_CONTENT_TYPE
+ redefine
+ default_create
+ end
+
+feature {NONE} -- Initialization
+
+ default_create
+ do
+ Precursor
+ create available_formats.make (1)
+ end
+
+feature -- Access
+
+ available_formats: ARRAYED_LIST [CONTENT_FORMAT]
+ -- Available formats for Current type.
+
+feature -- Factory
+
+ new_node_with_title (a_title: READABLE_STRING_32; a_partial_node: detachable CMS_NODE): like new_node
+ -- New node with `a_title' and fill from partial `a_partial_node' if set.
+ deferred
+ end
+
+ new_node (a_partial_node: detachable CMS_NODE): G
+ -- New node based on partial `a_partial_node' if set.
+ deferred
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/modules/node/content/cms_node.e b/modules/node/content/cms_node.e
new file mode 100644
index 0000000..0368c0e
--- /dev/null
+++ b/modules/node/content/cms_node.e
@@ -0,0 +1,291 @@
+note
+ description: "[
+ CMS abstraction for CMS content entity, named "node".
+ ]"
+ status: "draft"
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+deferred class
+ CMS_NODE
+
+inherit
+ CMS_CONTENT
+ rename
+ has_identifier as has_id
+ redefine
+ debug_output, has_id
+ end
+
+ REFACTORING_HELPER
+
+feature{NONE} -- Initialization
+
+ make_empty
+ -- Create empty node.
+ do
+ make ({STRING_32} "")
+ end
+
+ make (a_title: READABLE_STRING_32)
+ -- Create current node with `a_title'.
+ local
+ l_time: DATE_TIME
+ do
+ create l_time.make_now_utc
+ set_title (a_title)
+ set_creation_date (l_time)
+ set_modification_date (l_time)
+ set_publication_date (l_time)
+ mark_not_published
+ ensure
+ title_set: title = a_title
+ end
+
+feature -- Conversion
+
+ import_node (a_node: CMS_NODE)
+ -- Import `a_node' into current node.
+ do
+ set_id (a_node.id)
+ set_revision (a_node.revision)
+ set_title (a_node.title)
+ set_creation_date (a_node.creation_date)
+ set_modification_date (a_node.modification_date)
+ set_publication_date (a_node.publication_date)
+ set_author (a_node.author)
+ set_content (
+ a_node.content,
+ a_node.summary,
+ a_node.format
+ )
+ set_status (a_node.status)
+ set_link (a_node.link)
+ end
+
+feature -- Access
+
+ identifier: detachable IMMUTABLE_STRING_32
+ -- Optional identifier.
+ do
+ create Result.make_from_string_general (id.out)
+ end
+
+ id: INTEGER_64 assign set_id
+ -- Unique id.
+ --| Should we use NATURAL_64 instead?
+
+ revision: INTEGER_64 assign set_revision
+ -- Revision value.
+ --| Note: for now version is not supported.
+
+feature -- Status reports
+
+ status: INTEGER
+ -- Associated status for the current node.
+ -- default: {CMS_NODE_API}.Not_Published}
+ -- {CMS_NODE_API}.Published
+ -- {CMS_NODE_API}.Trashed
+
+ is_published: BOOLEAN
+ -- Is Current published?
+ do
+ Result := status = {CMS_NODE_API}.published
+ ensure
+ Result implies not is_trashed
+ end
+
+ is_trashed: BOOLEAN
+ -- Is Current trashed?
+ do
+ Result := status = {CMS_NODE_API}.trashed
+ ensure
+ Result implies not is_published
+ end
+
+feature -- Access
+
+ title: READABLE_STRING_32
+ -- Full title of the node.
+ -- Required!
+
+ summary: detachable READABLE_STRING_32
+ -- A short summary of the node.
+ deferred
+ end
+
+ content: detachable READABLE_STRING_32
+ -- Content of the node.
+ deferred
+ end
+
+feature -- Access: date
+
+ modification_date: DATE_TIME
+ -- When the node was updated.
+
+ creation_date: DATE_TIME
+ -- When the node was created.
+
+ publication_date: DATE_TIME
+ -- When the node was published.
+
+ publication_date_output: READABLE_STRING_32
+ -- Formatted output.
+
+feature -- Access: author
+
+ author: detachable CMS_USER
+ -- Author of current node.
+
+feature -- status report
+
+ has_id: BOOLEAN
+ -- Has unique identifier?
+ do
+ Result := id > 0
+ end
+
+ same_node (a_node: CMS_NODE): BOOLEAN
+ -- Is `a_node' same node as Current?
+ do
+ -- FIXME: if we introduce notion of revision, update this part!
+ Result := Current = a_node or id = a_node.id
+ ensure
+ valid_result: Result implies a_node.id = id
+ end
+
+feature -- Access: menu
+
+ link: detachable CMS_LOCAL_LINK
+ -- Associated menu link.
+
+feature -- Status report
+
+ debug_output: STRING_32
+ --
+ do
+ create Result.make_from_string_general ("#")
+ Result.append_integer_64 (id)
+ Result.append_character (' ')
+ Result.append (Precursor)
+ end
+
+feature -- Element change
+
+ set_content (a_content: like content; a_summary: like summary; a_format: like format)
+ -- Set `content', `summary' and `format' to `a_content', `a_summary' and `a_format'.
+ -- The `format' is associated with both `content' and `summary'
+ deferred
+ ensure
+ content_assigned: content = a_content
+ summary_assigned: summary = a_summary
+ format_assigned: format = a_format
+ end
+
+ set_title (a_title: like title)
+ -- Assign `title' with `a_title'.
+ do
+ title := a_title
+ if attached link as lnk then
+ lnk.set_title (a_title)
+ end
+ ensure
+ title_assigned: title = a_title
+ end
+
+ set_modification_date (a_modification_date: like modification_date)
+ -- Assign `modification_date' with `a_modification_date'.
+ do
+ modification_date := a_modification_date
+ ensure
+ modification_date_assigned: modification_date = a_modification_date
+ end
+
+ set_creation_date (a_creation_date: like creation_date)
+ -- Assign `creation_date' with `a_creation_date'.
+ do
+ creation_date := a_creation_date
+ ensure
+ creation_date_assigned: creation_date = a_creation_date
+ end
+
+ set_publication_date (a_publication_date: like publication_date)
+ -- Assign `publication_date' with `a_publication_date'.
+ do
+ publication_date := a_publication_date
+ publication_date_output := publication_date.formatted_out ("yyyy/[0]mm/[0]dd")
+ ensure
+ publication_date_assigned: publication_date = a_publication_date
+ end
+
+ set_id (a_id: like id)
+ -- Assign `id' with `a_id'.
+ do
+ id := a_id
+ ensure
+ id_assigned: id = a_id
+ end
+
+ set_revision (a_revision: like revision)
+ -- Assign `revision' with `a_revision'.
+ do
+ revision := a_revision
+ ensure
+ revision_assigned: revision = a_revision
+ end
+
+ set_author (u: like author)
+ -- Assign 'author' with `u'
+ do
+ author := u
+ ensure
+ auther_set: author = u
+ end
+
+ set_link (a_link: like link)
+ -- Set `link' to `a_link'.
+ do
+ link := a_link
+ end
+
+feature -- Status change
+
+ mark_not_published
+ -- Set status to not_published.
+ do
+ set_status ({CMS_NODE_API}.not_published)
+ ensure
+ status_not_published: status = {CMS_NODE_API}.not_published
+ end
+
+ mark_published
+ -- Set status to published.
+ do
+ set_status ({CMS_NODE_API}.published)
+ ensure
+ status_published: status = {CMS_NODE_API}.published
+ end
+
+ mark_trashed
+ -- Set status to trashed.
+ do
+ set_status ({CMS_NODE_API}.trashed)
+ ensure
+ status_trash: status = {CMS_NODE_API}.trashed
+ end
+
+feature {CMS_NODE_STORAGE_I} -- Access: status change.
+
+ set_status (a_status: like status)
+ -- Assign `status' with `a_status'.
+ do
+ status := a_status
+ ensure
+ status_set: status = a_status
+ end
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/modules/node/content/cms_partial_node.e b/modules/node/content/cms_partial_node.e
new file mode 100644
index 0000000..0cba1f6
--- /dev/null
+++ b/modules/node/content/cms_partial_node.e
@@ -0,0 +1,85 @@
+note
+ description: "Node object representing the CMS_NODE data in database."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PARTIAL_NODE
+
+inherit
+ CMS_NODE
+ rename
+ make_empty as make_empty_node,
+ set_content as set_all_content
+ end
+
+create
+ make_empty
+
+feature {NONE} -- Initialization
+
+ make_empty (a_content_type: READABLE_STRING_8)
+ require
+ type_not_blank: not a_content_type.is_whitespace
+ do
+ content_type := a_content_type
+ make_empty_node
+ end
+
+feature -- Access: code
+
+ content_type: READABLE_STRING_8
+ --
+
+feature -- Access: content
+
+ summary: detachable READABLE_STRING_32
+ -- A short summary of the node.
+
+ content: detachable READABLE_STRING_32
+ -- Content of the node.
+
+ format: detachable READABLE_STRING_8
+ -- Format associated with `content' and `summary'.
+ -- For example: text, mediawiki, html, etc
+
+feature -- Element change
+
+ set_all_content (a_content: like content; a_summary: like summary; a_format: like format)
+ --
+ do
+ set_content (a_content)
+ set_summary (a_summary)
+ set_format (a_format)
+ end
+
+ set_content (a_content: like content)
+ -- Assign `content' with `a_content', and set the associated `format'.
+ do
+ content := a_content
+ ensure
+ content_assigned: content = a_content
+ end
+
+ set_summary (a_summary: like summary)
+ -- Assign `summary' with `a_summary'.
+ do
+ summary := a_summary
+ ensure
+ summary_assigned: summary = a_summary
+ end
+
+ set_format (a_format: like format)
+ -- Assign `format' with `a_format'.
+ do
+ format := a_format
+ ensure
+ format_assigned: format = a_format
+ end
+
+invariant
+
+note
+ copyright: "2011-2015, Javier Velilla, Jocelyn Fiat, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+end
diff --git a/modules/node/content_type/cms_page.e b/modules/node/content_type/cms_page.e
new file mode 100644
index 0000000..d90f194
--- /dev/null
+++ b/modules/node/content_type/cms_page.e
@@ -0,0 +1,95 @@
+note
+ description: "A page node."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PAGE
+
+inherit
+ CMS_NODE
+ redefine
+ make_empty,
+ import_node
+ end
+
+create
+ make_empty,
+ make
+
+feature {NONE} -- Initialization
+
+ make_empty
+ do
+ Precursor
+ end
+
+feature -- Conversion
+
+ import_node (a_node: CMS_NODE)
+ --
+ do
+ Precursor (a_node)
+ if attached {CMS_PAGE} a_node as l_page then
+ set_parent (l_page.parent)
+ end
+ end
+
+feature -- Access
+
+ content_type: READABLE_STRING_8
+ once
+ Result := {CMS_PAGE_NODE_TYPE}.name
+ end
+
+feature -- Access: content
+
+ summary: detachable READABLE_STRING_32
+ -- A short summary of the node.
+
+ content: detachable READABLE_STRING_32
+ -- Content of the node.
+
+ format: detachable READABLE_STRING_8
+ -- Format associated with `content' and `summary'.
+ -- For example: text, mediawiki, html, etc
+
+ parent: detachable CMS_PAGE
+ -- Eventual parent page.
+ --| Used to describe a book structure.
+
+feature -- Element change
+
+ set_content (a_content: like content; a_summary: like summary; a_format: like format)
+ do
+ content := a_content
+ summary := a_summary
+ format := a_format
+ end
+
+ set_parent (a_page: detachable CMS_PAGE)
+ -- Set `parent' to `a_page'
+ require
+ Current_is_not_parent_of_a_page: not is_parent_of (a_page)
+ do
+ parent := a_page
+ end
+
+feature -- Status report
+
+ is_parent_of (a_page: detachable CMS_PAGE): BOOLEAN
+ -- Is Current page, a parent of `a_page' ?
+ do
+ if
+ a_page /= Void and then
+ attached a_page.parent as l_parent
+ then
+ if l_parent.same_node (a_page) then
+ Result := True
+ else
+ Result := is_parent_of (l_parent)
+ end
+ end
+ end
+
+end
diff --git a/modules/node/content_type/cms_page_node_type.e b/modules/node/content_type/cms_page_node_type.e
new file mode 100644
index 0000000..b336c77
--- /dev/null
+++ b/modules/node/content_type/cms_page_node_type.e
@@ -0,0 +1,46 @@
+note
+ description: "[
+ Interface defining a CMS page type.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PAGE_NODE_TYPE
+
+inherit
+ CMS_NODE_TYPE [CMS_PAGE]
+
+feature -- Access
+
+ name: STRING = "page"
+ -- Internal name.
+
+ title: STRING_32 = "Page"
+ -- Human readable name.
+
+ description: STRING_32 = "Use basic pages for your content, such as an 'About us' page."
+ -- Optional description
+
+feature -- Factory
+
+ new_node_with_title (a_title: READABLE_STRING_32; a_partial_node: detachable CMS_NODE): like new_node
+ -- New node with `a_title' and fill from partial `a_partial_node' if set.
+ do
+ create Result.make (a_title)
+ if a_partial_node /= Void then
+ Result.import_node (a_partial_node)
+ Result.set_title (a_title)
+ end
+ end
+
+ new_node (a_partial_node: detachable CMS_NODE): CMS_PAGE
+ -- New node based on partial `a_partial_node', or from none.
+ do
+ create Result.make_empty
+ if a_partial_node /= Void then
+ Result.import_node (a_partial_node)
+ end
+ end
+
+end
diff --git a/modules/node/content_type/cms_partial_page.e b/modules/node/content_type/cms_partial_page.e
new file mode 100644
index 0000000..fc2dd7b
--- /dev/null
+++ b/modules/node/content_type/cms_partial_page.e
@@ -0,0 +1,17 @@
+note
+ description: "Summary description for {CMS_PARTIAL_PAGE}."
+ author: ""
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PARTIAL_PAGE
+
+inherit
+ CMS_PAGE
+
+create
+ make_empty,
+ make
+
+end
diff --git a/modules/node/export/cms_export_node_utilities.e b/modules/node/export/cms_export_node_utilities.e
new file mode 100644
index 0000000..c215d52
--- /dev/null
+++ b/modules/node/export/cms_export_node_utilities.e
@@ -0,0 +1,55 @@
+note
+ description: "[
+ Routines usefull during node exportation (see {CMS_HOOK_EXPORT}).
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_EXPORT_NODE_UTILITIES
+
+inherit
+ CMS_EXPORT_JSON_UTILITIES
+
+feature -- Access
+
+ node_to_json (n: CMS_NODE): JSON_OBJECT
+ local
+ jo,j_author: JSON_OBJECT
+ do
+ create Result.make_empty
+ Result.put_string (n.content_type, "type")
+ Result.put_integer (n.id, "nid")
+ Result.put_integer (n.revision, "revision")
+ Result.put_string (n.title, "title")
+ put_date_into_json (n.creation_date, "creation_date", Result)
+ put_date_into_json (n.modification_date, "modification_date", Result)
+ put_date_into_json (n.publication_date, "publication_date", Result)
+ Result.put_integer (n.status, "status")
+ if attached n.author as u then
+ create j_author.make
+ j_author.put_integer (u.id, "uid")
+ j_author.put_string (u.name, "name")
+ Result.put (j_author, "author")
+ end
+ create jo.make_empty
+ if attached n.format as l_format then
+ jo.put_string (l_format, "format")
+ end
+ if attached n.summary as s then
+ jo.put_string (s, "summary")
+ end
+ if attached n.content as s then
+ jo.put_string (s, "content")
+ end
+ Result.put (jo, "data")
+ if attached n.link as lnk then
+ create jo.make_empty
+ jo.put_string (lnk.title, "title")
+ jo.put_string (lnk.location, "location")
+ jo.put_integer (lnk.weight, "weight")
+ Result.put (jo, "link")
+ end
+ end
+
+end
diff --git a/modules/node/handler/cms_node_handler.e b/modules/node/handler/cms_node_handler.e
new file mode 100644
index 0000000..e426044
--- /dev/null
+++ b/modules/node/handler/cms_node_handler.e
@@ -0,0 +1,16 @@
+note
+ description: "Summary description for {CMS_NODE_HANDLER}."
+ author: ""
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+deferred class
+ CMS_NODE_HANDLER
+
+inherit
+ CMS_MODULE_HANDLER [CMS_NODE_API]
+ rename
+ module_api as node_api
+ end
+
+end
diff --git a/modules/node/handler/cms_node_type_webform_manager.e b/modules/node/handler/cms_node_type_webform_manager.e
new file mode 100644
index 0000000..10e866d
--- /dev/null
+++ b/modules/node/handler/cms_node_type_webform_manager.e
@@ -0,0 +1,367 @@
+note
+ description: "Summary description for {CMS_NODE_TYPE_WEBFORM_MANAGER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_NODE_TYPE_WEBFORM_MANAGER [G -> CMS_NODE]
+
+inherit
+ CMS_NODE_TYPE_WEBFORM_MANAGER_I [G]
+
+ SHARED_WSF_PERCENT_ENCODER
+
+feature -- Forms ...
+
+ populate_form (response: NODE_RESPONSE; f: CMS_FORM; a_node: detachable CMS_NODE)
+ local
+ ti: WSF_FORM_TEXT_INPUT
+ fset: WSF_FORM_FIELD_SET
+ ta, sum: CMS_FORM_TEXTAREA
+ tselect: WSF_FORM_SELECT
+ opt: WSF_FORM_SELECT_OPTION
+ cms_format: CMS_EDITOR_CONTENT_FORMAT
+ do
+ create cms_format
+
+ create ti.make ("title")
+ ti.enable_required
+ ti.set_label ("Title")
+ ti.set_size (70)
+ if a_node /= Void then
+ ti.set_text_value (a_node.title)
+ end
+ f.extend (ti)
+
+ f.extend_html_text (" ")
+
+ -- Select field has to be initialized before textareas are replaced, because they depend on the selection of the field
+ create tselect.make ("format")
+ tselect.set_label ("Format for content (and summary)")
+ tselect.set_is_required (True)
+
+ -- Main Content
+ create ta.make ("content")
+ ta.set_rows (10)
+ ta.set_cols (70)
+ ta.show_as_editor_if_selected (tselect, cms_format.name)
+ if a_node /= Void then
+ ta.set_text_value (a_node.content)
+ end
+ ta.set_label (response.translation ("Content", Void))
+ ta.set_description (response.translation ("This is the main content", Void))
+ ta.set_is_required (False)
+
+ -- Summary
+ create sum.make ("summary")
+ sum.set_rows (3)
+ sum.set_cols (70)
+ -- if cms_html is selected
+ sum.show_as_editor_if_selected (tselect, cms_format.name)
+ if a_node /= Void then
+ sum.set_text_value (a_node.summary)
+ end
+ sum.set_label (response.translation ("Summary", Void))
+ sum.set_description (response.translation ("Text displayed in short view.", Void))
+ sum.set_is_required (False)
+
+ create fset.make
+
+ -- Add summary
+ fset.extend (sum)
+ fset.extend_html_text(" ")
+
+ -- Add content
+ fset.extend (ta)
+ fset.extend_html_text (" ")
+
+ across
+ content_type.available_formats as c
+ loop
+ create opt.make (c.item.name, c.item.title)
+ if attached c.item.html_help as f_help then
+ opt.set_description ("")
+ end
+ tselect.add_option (opt)
+ end
+ if a_node /= Void and then attached a_node.format as l_format then
+ tselect.set_text_by_value (l_format)
+ end
+
+ fset.extend (tselect)
+
+ f.extend (fset)
+
+ -- Path alias
+ populate_form_with_taxonomy (response, f, a_node)
+ populate_form_with_path_alias (response, f, a_node)
+ end
+
+ populate_form_with_taxonomy (response: CMS_RESPONSE; f: CMS_FORM; a_content: detachable CMS_CONTENT)
+ do
+ if attached {CMS_TAXONOMY_API} response.api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api then
+ l_taxonomy_api.populate_edit_form (response, f, content_type.name, a_content)
+ end
+ end
+
+ populate_form_with_path_alias (response: NODE_RESPONSE; f: CMS_FORM; a_node: detachable CMS_NODE)
+ local
+ ti: WSF_FORM_TEXT_INPUT
+ l_uri: detachable READABLE_STRING_8
+ l_iri: detachable READABLE_STRING_32
+ do
+ -- Path alias
+ create ti.make ("path_alias")
+ ti.set_label ("Path")
+ ti.set_pattern ("^([A-Za-z0-9-_+ ]).+")
+ ti.set_description ("Path alias pattern: ^([A-Za-z0-9-_+ ]).+ For example resource/page1 ")
+ ti.set_size (70)
+ if a_node /= Void and then a_node.has_id then
+ if attached a_node.link as lnk then
+ l_uri := lnk.location
+ else
+ l_iri := percent_encoder.percent_decoded_string (response.api.location_alias (response.node_api.node_path (a_node)))
+ l_uri := l_iri.to_string_8
+ end
+ ti.set_text_value (l_uri)
+ ti.set_description ("Optionally specify an alternative URL path by which this content can be accessed. For example, type 'about' when writing an about page. Use a relative path or the URL alias won't work.")
+ end
+ ti.set_validation_action (agent (fd: WSF_FORM_DATA; ia_response: NODE_RESPONSE; ia_node: detachable CMS_NODE)
+ do
+ if
+ attached fd.string_item ("path_alias") as f_path_alias
+ then
+ if ia_response.api.is_valid_path_alias (f_path_alias) then
+ -- Ok.
+ elseif f_path_alias.is_empty then
+ -- Ok
+ elseif f_path_alias.starts_with_general ("/") then
+ fd.report_invalid_field ("path_alias", "Path alias should not start with a slash '/' .")
+ elseif f_path_alias.has_substring ("://") then
+ fd.report_invalid_field ("path_alias", "Path alias should not be absolute url .")
+ else
+ -- TODO: implement full path alias validation
+ end
+ if
+ attached ia_response.api.source_of_path_alias (f_path_alias) as l_aliased_location and then
+ ((ia_node /= Void and then ia_node.has_id) implies not l_aliased_location.same_string (ia_response.node_api.node_path (ia_node)))
+ then
+ fd.report_invalid_field ("path_alias", "Path is already aliased to location %"" + ia_response.link (Void, l_aliased_location, Void) + "%" !")
+ end
+ end
+ end(?, response, a_node)
+ )
+ if
+ attached f.fields_by_name ("title") as l_title_fields and then
+ attached l_title_fields.first as l_title_field
+ then
+ f.insert_after (ti, l_title_field)
+ else
+ f.extend (ti)
+ end
+ end
+
+ update_node (response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: CMS_NODE)
+ local
+ b,s: detachable READABLE_STRING_32
+ f: detachable CONTENT_FORMAT
+ do
+ if attached fd.integer_item ("id") as l_id and then l_id > 0 then
+ check a_node.id = l_id end
+ end
+ if attached fd.string_item ("title") as l_title then
+ a_node.set_title (l_title)
+ end
+
+ if attached fd.string_item ("content") as l_content then
+ b := l_content
+ end
+
+ -- Read out the summary field from the form data
+ if attached fd.string_item ("summary") as l_summary then
+ s := l_summary
+ end
+
+ if attached fd.string_item ("format") as s_format and then attached response.api.format (s_format) as f_format then
+ f := f_format
+ elseif a_node /= Void and then attached a_node.format as s_format and then attached response.api.format (s_format) as f_format then
+ f := f_format
+ else
+ f := cms_api.formats.default_format
+ end
+
+ -- Update node with summary and body content
+ if b /= Void then
+ a_node.set_content (b, s, f.name)
+ end
+
+ -- Update author
+ a_node.set_author (response.user)
+ end
+
+ new_node (response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: detachable CMS_NODE): G
+ --
+ local
+ b,s: detachable READABLE_STRING_32
+ f: detachable CONTENT_FORMAT
+ l_node: detachable like new_node
+ do
+ if attached {like new_node} a_node as l_arg_node then
+ l_node := l_arg_node
+ else
+ l_node := content_type.new_node (a_node)
+ end
+ if attached fd.integer_item ("id") as l_id and then l_id > 0 then
+ if l_node /= Void then
+ check l_node.id = l_id end
+ else
+ if attached {like new_node} response.node_api.node (l_id) as n then
+ l_node := n
+ else
+ -- FIXME: Error
+ end
+ end
+ end
+ if attached fd.string_item ("title") as l_title then
+ if l_node = Void then
+ l_node := content_type.new_node (Void)
+ l_node.set_title (l_title)
+ else
+ l_node.set_title (l_title)
+ end
+ else
+ if l_node = Void then
+ l_node := content_type.new_node_with_title ("...", Void)
+ end
+ end
+ l_node.set_author (response.user)
+
+ --Summary
+ if attached fd.string_item ("summary") as l_summary then
+ s := l_summary
+ end
+
+ --Content
+ if attached fd.string_item ("content") as l_content then
+ b := l_content
+ end
+
+ if attached fd.string_item ("format") as s_format and then attached response.api.format (s_format) as f_format then
+ f := f_format
+ elseif a_node /= Void and then attached a_node.format as s_format and then attached response.api.format (s_format) as f_format then
+ f := f_format
+ else
+ f := cms_api.formats.default_format
+ end
+
+ -- Update node with summary and content
+ if b /= Void then
+ l_node.set_content (b, s, f.name)
+ end
+ Result := l_node
+ end
+
+feature -- Output
+
+ append_content_as_html_to (a_node: G; is_teaser: BOOLEAN; a_output: STRING; a_response: detachable CMS_RESPONSE)
+ --
+ local
+ lnk: detachable CMS_LOCAL_LINK
+ hdate: HTTP_DATE
+ l_node_api: CMS_NODE_API
+ do
+ l_node_api := node_api
+
+ -- Show tabs only if a user is authenticated.
+ if
+ not is_teaser and then
+ a_response /= Void and then
+ attached a_response.user as l_user
+ then
+ lnk := a_node.link
+ if lnk /= Void then
+ lnk := a_response.local_link (a_response.translation ("View", Void), lnk.location)
+ else
+ lnk := a_response.local_link (a_response.translation ("View", Void), l_node_api.node_path (a_node))
+ end
+ lnk.set_weight (1)
+ a_response.add_to_primary_tabs (lnk)
+
+ if a_node.status = {CMS_NODE_API}.trashed then
+ create lnk.make ("Delete", l_node_api.node_path (a_node) + "/delete")
+ lnk.set_weight (2)
+ a_response.add_to_primary_tabs (lnk)
+ elseif a_node.has_id then
+ -- Node in {{CMS_NODE_API}.published} or {CMS_NODE_API}.not_published} status.
+ create lnk.make ("Edit", l_node_api.node_path (a_node) + "/edit")
+ lnk.set_weight (2)
+ a_response.add_to_primary_tabs (lnk)
+ if
+ l_node_api.has_permission_for_action_on_node ("view revisions", a_node, l_user)
+ then
+ create lnk.make ("Revisions", l_node_api.node_path (a_node) + "/revision")
+ lnk.set_weight (3)
+ a_response.add_to_primary_tabs (lnk)
+ end
+
+ if
+ l_node_api.has_permission_for_action_on_node ("trash", a_node, l_user)
+ then
+ create lnk.make ("Move to trash", l_node_api.node_path (a_node) + "/trash")
+ lnk.set_weight (3)
+ a_response.add_to_primary_tabs (lnk)
+ end
+ end
+ end
+ a_output.append ("")
+
+ a_output.append ("
")
+ if attached a_node.author as l_author then
+ a_output.append (" by ")
+ a_output.append (l_node_api.html_encoded (l_author.name))
+ end
+ if attached a_node.modification_date as l_modified then
+ a_output.append (" (modified: ")
+ create hdate.make_from_date_time (l_modified)
+ a_output.append (hdate.yyyy_mmm_dd_string)
+ a_output.append (")")
+ end
+ a_output.append ("
")
+
+ if
+ a_response /= Void and then
+ attached {CMS_TAXONOMY_API} cms_api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api
+ then
+ l_taxonomy_api.append_taxonomy_to_xhtml (a_node, a_response, a_output)
+ end
+
+ -- We don't show the summary on the detail page, since its just a short view of the full content. Otherwise we would write the same thing twice.
+ -- The usage of the summary is to give a short overview in the list of nodes or for the meta tag "description"
+ if is_teaser then
+ if attached a_node.summary as l_summary then
+ a_output.append ("
")
+ if attached cms_api.format (a_node.format) as f then
+ append_formatted_content_to (l_summary, f, a_output)
+ else
+ append_formatted_content_to (l_summary, cms_api.formats.default_format, a_output)
+ end
+ a_output.append ("
")
+ end
+ elseif attached a_node.content as l_content then
+ a_output.append ("
")
+ if attached cms_api.format (a_node.format) as f then
+ append_formatted_content_to (l_content, f, a_output)
+ else
+ append_formatted_content_to (l_content, cms_api.formats.default_format, a_output)
+ end
+ a_output.append ("
")
+ end
+ a_output.append ("
")
+ end
+
+end
+
diff --git a/modules/node/handler/cms_node_type_webform_manager_i.e b/modules/node/handler/cms_node_type_webform_manager_i.e
new file mode 100644
index 0000000..87dda89
--- /dev/null
+++ b/modules/node/handler/cms_node_type_webform_manager_i.e
@@ -0,0 +1,93 @@
+note
+ description: "[
+ Html builder for content type `content_type'.
+ This is used to build webform and html output for a specific node, or node content type.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_NODE_TYPE_WEBFORM_MANAGER_I [G -> CMS_NODE]
+
+inherit
+ CMS_CONTENT_TYPE_WEBFORM_MANAGER [CMS_NODE]
+ rename
+ make as old_make
+ redefine
+ content_type
+ end
+
+feature {NONE} -- Initialization
+
+ make (a_type: like content_type; a_node_api: CMS_NODE_API)
+ do
+ node_api := a_node_api
+ old_make (a_type)
+ end
+
+feature -- Access
+
+ content_type: CMS_NODE_TYPE [G]
+ -- Associated content type.
+
+ cms_api: CMS_API
+ -- API for current instance of CMS.
+ do
+ Result := node_api.cms_api
+ end
+
+ node_api: CMS_NODE_API
+ -- Associated node API.
+
+feature -- Query
+
+ has_valid_node_type (a_node: CMS_NODE): BOOLEAN
+ -- Accept `a_node' for Current operations.
+ do
+ Result := attached {like new_node} a_node
+ end
+
+feature -- Forms ...
+
+ populate_form (response: NODE_RESPONSE; a_form: WSF_FORM; a_node: detachable CMS_NODE)
+ -- Fill the web form `a_form' with data from `a_node' if set.
+ require
+ a_node = Void or else has_valid_node_type (a_node)
+ deferred
+ end
+
+feature -- Node ...
+
+ new_node (response: NODE_RESPONSE; a_form_data: WSF_FORM_DATA; a_node: detachable CMS_NODE): G
+ -- New typed node with data from `a_form_data', and eventually data from `a_node' if set.
+ require
+ a_node = Void or else has_valid_node_type (a_node)
+ deferred
+ --| Possible implementation:
+ --| Result := content_type.new_node (a_node)
+ end
+
+ update_node (response: NODE_RESPONSE; a_form_data: WSF_FORM_DATA; a_node: CMS_NODE)
+ -- Update node `a_node' with data from `a_form_data'.
+ require
+ has_valid_node_type (a_node)
+ deferred
+ end
+
+feature -- Output
+
+ append_content_as_html_to_page (a_node: G; a_response: NODE_RESPONSE)
+ -- Append an html representation of `a_node' to response `a_response'.
+ require
+ has_valid_node_type (a_node)
+ local
+ s: STRING
+ do
+ create s.make_empty
+ a_response.set_value (a_node, "node")
+ a_response.set_title (a_node.title)
+ append_content_as_html_to (a_node, False, s, a_response)
+ a_response.set_main_content (s)
+ end
+
+end
diff --git a/modules/node/handler/cms_page_node_type_webform_manager.e b/modules/node/handler/cms_page_node_type_webform_manager.e
new file mode 100644
index 0000000..090e65e
--- /dev/null
+++ b/modules/node/handler/cms_page_node_type_webform_manager.e
@@ -0,0 +1,187 @@
+note
+ description: "Summary description for {CMS_PAGE_NODE_TYPE_WEBFORM_MANAGER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_PAGE_NODE_TYPE_WEBFORM_MANAGER
+
+inherit
+ CMS_NODE_TYPE_WEBFORM_MANAGER [CMS_PAGE]
+ redefine
+ content_type,
+ append_content_as_html_to,
+ populate_form,
+ new_node,
+ update_node
+ end
+
+create
+ make
+
+feature -- Access
+
+ content_type: CMS_PAGE_NODE_TYPE
+ -- Associated content type.
+
+feature -- Forms ...
+
+ populate_form (response: NODE_RESPONSE; f: CMS_FORM; a_node: detachable CMS_NODE)
+ local
+ ti: WSF_FORM_NUMBER_INPUT
+ fs: WSF_FORM_FIELD_SET
+ l_parent_id: INTEGER_64
+ do
+ Precursor (response, f, a_node)
+
+ if attached {CMS_PAGE} a_node as l_page then
+ create fs.make
+ fs.set_legend ("Pages structure")
+ fs.set_collapsible (True)
+ f.extend (fs)
+ create ti.make ("select_parent_node")
+ ti.set_label ("Parent page")
+ ti.set_description ("The parent page is the book structure.")
+ if attached l_page.parent as l_parent_node then
+ l_parent_id := l_parent_node.id
+ fs.extend_html_text ("Currently, the parent page is ")
+ fs.extend_html_text (response.node_html_link (l_parent_node, l_parent_node.title))
+ fs.extend_html_text ("
")
+ end
+ ti.set_validation_action (agent parent_validation (response, ?))
+ fs.extend (ti)
+
+ -- FIXME: add notion of "weight"
+
+ if
+ attached {WSF_STRING} response.request.query_parameter ("parent") as p_parent and then
+ p_parent.is_integer
+ then
+ l_parent_id := p_parent.integer_value.to_integer_64
+ end
+ if l_parent_id > 0 then
+ ti.set_default_value (l_parent_id.out)
+ end
+ end
+ end
+
+ update_node (a_response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: CMS_NODE)
+ --
+ local
+ l_parent_id: INTEGER_64
+ do
+ Precursor (a_response, fd, a_node)
+ if attached {CMS_PAGE} a_node as l_page then
+ if attached fd.integer_item ("select_parent_node") as i_parent_node then
+ l_parent_id := i_parent_node.to_integer_64
+ end
+ if
+ l_parent_id > 0 and then
+ attached {CMS_PAGE} a_response.node_api.node (l_parent_id) as l_parent_page
+ then
+ l_page.set_parent (l_parent_page)
+ elseif l_parent_id = -1 then
+ -- Set parent to Void
+ l_page.set_parent (Void)
+ end
+ end
+ end
+
+ new_node (response: NODE_RESPONSE; fd: WSF_FORM_DATA; a_node: detachable like new_node): like content_type.new_node
+ --
+ do
+ Result := Precursor (response, fd, a_node)
+ if attached fd.integer_item ("select_parent_node") as l_parent_id then
+ if l_parent_id = -1 then
+ Result.set_parent (Void)
+ elseif attached {CMS_PAGE} response.node_api.node (l_parent_id) as l_parent then
+ Result.set_parent (l_parent)
+ end
+ end
+ end
+
+ parent_validation (a_response: NODE_RESPONSE; fd: WSF_FORM_DATA)
+ local
+ l_node_api: CMS_NODE_API
+ l_parent_id: INTEGER_64
+ nid: INTEGER_64
+ l_parent_node: detachable CMS_NODE
+ do
+ l_node_api := node_api
+ if attached fd.integer_item ("select_parent_node") as s_parent_node then
+ l_parent_id := s_parent_node.to_integer_64
+ else
+ l_parent_id := 0
+ end
+ if l_parent_id > 0 then
+ l_parent_node := l_node_api.node (l_parent_id)
+ if l_parent_node = Void then
+ fd.report_invalid_field ("select_parent_node", "Invalid parent, not found id #" + l_parent_id.out)
+ else
+ nid := a_response.node_id_path_parameter
+ if
+ nid > 0 and then
+ attached l_node_api.node (nid) as l_node and then
+ l_node_api.is_node_a_parent_of (l_node, l_parent_node)
+ then
+ fd.report_invalid_field ("select_parent_node", "Invalid parent due to cycle (node #" + nid.out + " is already a parent of node #" + l_parent_id.out)
+ end
+ end
+ elseif l_parent_id = -1 or else l_parent_id = 0 then
+ -- -1 is Used to unassign a parent node
+ -- 0 is not taken into account, any other input value is considered invalid.
+ else
+ fd.report_invalid_field ("select_parent_node", "Invalid node id")
+ end
+ end
+
+feature -- Output
+
+ append_content_as_html_to (a_node: CMS_PAGE; is_teaser: BOOLEAN; a_output: STRING; a_response: detachable CMS_RESPONSE)
+ --
+ local
+ l_node_api: CMS_NODE_API
+ lnk: CMS_LOCAL_LINK
+ do
+ Precursor (a_node, is_teaser, a_output, a_response)
+
+ if not is_teaser then
+ l_node_api := node_api
+ if
+ a_response /= Void and then
+ a_node.has_id and then not a_node.is_trashed
+ then
+ if
+ l_node_api.has_permission_for_action_on_node ("create", a_node, a_response.user)
+ then
+ create lnk.make ("Add Child", "node/add/page?parent=" + a_node.id.out)
+ lnk.set_weight (3)
+ a_response.add_to_primary_tabs (lnk)
+ end
+ end
+
+ if
+ a_response /= Void and then
+ attached {CMS_PAGE} a_node as l_node_page
+ then
+ a_output.append ("")
+ if attached l_node_page.parent as l_parent_node then
+ a_output.append ("Go to parent page ")
+ a_output.append (a_response.link (l_parent_node.title, l_node_api.node_path (l_parent_node), Void))
+ a_output.append (" ")
+ end
+ if attached l_node_api.children (a_node) as l_children then
+ across
+ l_children as ic
+ loop
+ a_output.append ("")
+ a_output.append (a_response.link (ic.item.title, l_node_api.node_path (ic.item), Void))
+ a_output.append (" ")
+ end
+ end
+ a_output.append (" ")
+ end
+ end
+ end
+
+end
diff --git a/modules/node/handler/node_form_response.e b/modules/node/handler/node_form_response.e
new file mode 100644
index 0000000..604bcb5
--- /dev/null
+++ b/modules/node/handler/node_form_response.e
@@ -0,0 +1,443 @@
+note
+ description: "CMS Response handling node editing workflow using Web forms."
+ revision: "$Revision$"
+
+class
+ NODE_FORM_RESPONSE
+
+inherit
+ NODE_RESPONSE
+
+create
+ make
+
+feature -- Execution
+
+ process
+ -- Computed response message.
+ local
+ b: STRING_8
+ nid: INTEGER_64
+ do
+ create b.make_empty
+ nid := node_id_path_parameter
+ if
+ nid > 0 and then
+ attached node_api.node (nid) as l_node
+ then
+ if attached node_api.node_type_for (l_node) as l_type then
+ if
+ location.ends_with_general ("/edit") and then
+ node_api.has_permission_for_action_on_node ("edit", l_node, user)
+ then
+ edit_node (l_node, l_type, b)
+ elseif
+ location.ends_with_general ("/delete") and then
+ node_api.has_permission_for_action_on_node ("delete", l_node, user)
+ then
+ delete_node (l_node, l_type, b)
+ elseif
+ location.ends_with_general ("/trash") and then
+ node_api.has_permission_for_action_on_node ("trash", l_node, user)
+ then
+ trash_node (l_node, l_type, b)
+ else
+ b.append ("")
+ b.append (translation ("Access denied", Void))
+ b.append (" ")
+ end
+ else
+ set_title (translation ("Unknown node", Void))
+ end
+ elseif
+ attached {WSF_STRING} request.path_parameter ("type") as p_type and then
+ attached node_api.node_type (p_type.value) as l_type
+ then
+ if has_permissions (<<"create any", "create " + l_type.name>>) then
+ -- create new node
+ create_new_node (l_type, b)
+ else
+ b.append ("")
+ b.append (translation ("Access denied", Void))
+ b.append (" ")
+ end
+ else
+ set_title (translation ("Create new content ...", Void))
+ b.append ("")
+ across
+ node_api.node_types as ic
+ loop
+ if
+ attached ic.item as l_node_type and then
+ has_permissions (<<"create any", "create " + l_node_type.name>>)
+ then
+ b.append ("" + link (l_node_type.name, "node/add/" + l_node_type.name, Void))
+ if attached l_node_type.description as d then
+ b.append ("" + d + "
")
+ end
+ b.append (" ")
+ end
+ end
+ b.append (" ")
+ end
+ set_main_content (b)
+ end
+
+
+feature {NONE} -- Create a new node
+
+ create_new_node (a_type: CMS_NODE_TYPE [CMS_NODE]; b: STRING_8)
+ local
+ f: like new_edit_form
+ fd: detachable WSF_FORM_DATA
+ do
+ if attached a_type.new_node (Void) as l_node then
+ -- create new node
+ f := new_edit_form (l_node, url (location, Void), "edit-" + a_type.name, a_type)
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.validation_actions.extend (agent edit_form_validate (?, b))
+ f.submit_actions.put_front (agent edit_form_submit (?, l_node, a_type, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+
+ if l_node.has_id then
+ set_title ("Edit " + html_encoded (a_type.title) + " #" + l_node.id.out)
+ add_to_menu (node_local_link (l_node, translation ("View", Void)), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void), node_api.node_path (l_node) + "/edit"), primary_tabs)
+ else
+ set_title ("New " + html_encoded (a_type.title))
+ end
+ f.append_to_html (wsf_theme, b)
+ else
+ b.append ("")
+ b.append (translation ("Server error", Void))
+ b.append (" ")
+ end
+ end
+
+
+ edit_node (a_node: CMS_NODE; a_type: CMS_NODE_TYPE [CMS_NODE]; b: STRING_8)
+ local
+ f: like new_edit_form
+ fd: detachable WSF_FORM_DATA
+ do
+ f := new_edit_form (A_node, url (location, Void), "edit-" + a_type.name, a_type)
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.validation_actions.extend (agent edit_form_validate (?, b))
+ f.submit_actions.put_front (agent edit_form_submit (?, a_node, a_type, b))
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_node.has_id then
+ add_to_menu (node_local_link (a_node, translation ("View", Void)), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void), node_api.node_path (a_node) + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make ("Delete", node_api.node_path (a_node) + "/delete"), primary_tabs)
+ end
+
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_node.title)
+ b.append (html_encoded (a_type.title) + " saved")
+ else
+ set_title (formatted_string (translation ("Edit $1 #$2", Void), [a_type.title, a_node.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ end
+
+
+ delete_node (a_node: CMS_NODE; a_type: CMS_NODE_TYPE [CMS_NODE]; b: STRING_8)
+ local
+ f: like new_edit_form
+ fd: detachable WSF_FORM_DATA
+ do
+ if a_node.is_trashed then
+ f := new_delete_form (a_node, url (location, Void), "delete-" + a_type.name, a_type)
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_node.has_id then
+ add_to_menu (node_local_link (a_node, translation ("View", Void)), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make (translation ("Edit", Void), node_api.node_path (a_node) + "/edit"), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make ("Delete", node_api.node_path (a_node) + "/delete"), primary_tabs)
+ end
+
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_node.title)
+ b.append (html_encoded (a_type.title) + " deleted")
+ else
+ set_title (formatted_string (translation ("Delete $1 #$2", Void), [a_type.title, a_node.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ else
+ --
+ end
+ end
+
+
+ trash_node (a_node: CMS_NODE; a_type: CMS_NODE_TYPE [CMS_NODE]; b: STRING_8)
+ local
+ f: like new_edit_form
+ fd: detachable WSF_FORM_DATA
+ do
+ f := new_trash_form (a_node, url (location, Void), "trash-" + a_type.name, a_type)
+ api.hooks.invoke_form_alter (f, fd, Current)
+ if request.is_post_request_method then
+ f.process (Current)
+ fd := f.last_data
+ end
+ if a_node.has_id then
+ add_to_menu (node_local_link (a_node, translation ("View", Void)), primary_tabs)
+ add_to_menu (create {CMS_LOCAL_LINK}.make ("Trash", node_api.node_path (a_node) + "/Trash"), primary_tabs)
+ end
+
+ if attached redirection as l_location then
+ -- FIXME: Hack for now
+ set_title (a_node.title)
+ b.append (html_encoded (a_type.title) + " trashed")
+ else
+ set_title (formatted_string (translation ("Trash $1 #$2", Void), [a_type.title, a_node.id]))
+ f.append_to_html (wsf_theme, b)
+ end
+ end
+
+
+feature -- Form
+
+ edit_form_validate (fd: WSF_FORM_DATA; b: STRING)
+ local
+ l_preview: BOOLEAN
+ l_format: detachable CONTENT_FORMAT
+ do
+ l_preview := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Preview")
+ if l_preview then
+ b.append ("Preview ")
+ if attached fd.string_item ("format") as s_format and then attached api.format (s_format) as f_format then
+ l_format := f_format
+ end
+ if attached fd.string_item ("title") as l_title then
+ b.append ("
Title: " + html_encoded (l_title) + "
")
+ end
+ if attached fd.string_item ("body") as l_body then
+ b.append ("
Body: ")
+ if l_format /= Void then
+ b.append (l_format.formatted_output (l_body))
+ else
+ b.append (html_encoded (l_body))
+ end
+ b.append ("
")
+ end
+ b.append ("
")
+ end
+ end
+
+ edit_form_submit (fd: WSF_FORM_DATA; a_node: detachable CMS_NODE; a_type: CMS_NODE_TYPE [CMS_NODE]; b: STRING)
+ local
+ l_preview: BOOLEAN
+ l_node: detachable CMS_NODE
+ s: STRING
+ l_path_alias: detachable READABLE_STRING_8
+ do
+ fixme ("Refactor code per operacion: Preview, Save")
+ l_preview := attached {WSF_STRING} fd.item ("op") as l_op and then l_op.same_string ("Preview")
+ if not l_preview then
+ debug ("cms")
+ across
+ fd as c
+ loop
+ b.append ("" + html_encoded (c.key) + "=")
+ if attached c.item as v then
+ b.append (html_encoded (v.string_representation))
+ end
+ b.append (" ")
+ end
+ end
+ if a_node /= Void then
+ l_node := a_node
+ apply_form_data_to_node (a_type, fd, a_node)
+
+ if l_node.has_id then
+ s := "modified"
+ else
+ s := "created"
+ end
+ else
+ l_node := new_node (a_type, fd, Void)
+ s := "created"
+ end
+
+ fixme ("for now, publishing is not implemented, so let's assume any node saved is published.") -- FIXME
+ l_node.mark_published
+
+ node_api.save_node (l_node)
+ if attached user as u then
+ api.log ("node",
+ "User %"" + user_html_link (u) + "%" " + s + " node " + node_html_link (l_node, a_type.name + " #" + l_node.id.out),
+ 0, node_local_link (l_node, Void)
+ )
+ else
+ api.log ("node", "Anonymous " + s + " node " + a_type.name +" #" + l_node.id.out, 0, node_local_link (l_node, Void))
+ end
+ if node_api.has_error then
+ add_error_message ("Node #" + l_node.id.out + " failed to save.")
+ else
+ add_success_message ("Node #" + l_node.id.out + " saved.")
+ end
+
+ if
+ attached fd.string_item ("path_alias") as f_path_alias and then
+ not f_path_alias.is_empty
+ then
+ l_path_alias := percent_encoder.partial_encoded_string (f_path_alias, <<'/'>>)
+ -- Path alias, are always from the root of the cms.
+ api.set_path_alias (node_api.node_path (l_node), l_path_alias, False)
+ l_node.set_link (create {CMS_LOCAL_LINK}.make (l_node.title, l_path_alias))
+ else
+ l_node.set_link (node_api.node_link (l_node))
+ end
+ if attached l_node.link as lnk then
+ set_redirection (lnk.location)
+ end
+ end
+ end
+
+ new_edit_form (a_node: detachable CMS_NODE; a_url: READABLE_STRING_8; a_name: STRING; a_node_type: CMS_NODE_TYPE [CMS_NODE]): CMS_FORM
+ -- Create a web form named `a_name' for node `a_node' (if set), using form action url `a_url', and for type of node `a_node_type'.
+ local
+ f: CMS_FORM
+ ts: WSF_FORM_SUBMIT_INPUT
+ th: WSF_FORM_HIDDEN_INPUT
+ do
+ create f.make (a_url, a_name)
+ create th.make ("node-id")
+ if a_node /= Void then
+ th.set_text_value (a_node.id.out)
+ else
+ th.set_text_value ("0")
+ end
+ f.extend (th)
+
+ populate_form (a_node_type, f, a_node)
+
+ f.extend_html_text (" ")
+ create ts.make ("op")
+ ts.set_default_value ("Save")
+ f.extend (ts)
+
+ create ts.make ("op")
+ ts.set_default_value ("Preview")
+ f.extend (ts)
+
+ Result := f
+ end
+
+
+ new_delete_form (a_node: detachable CMS_NODE; a_url: READABLE_STRING_8; a_name: STRING; a_node_type: CMS_NODE_TYPE [CMS_NODE]): CMS_FORM
+ -- Create a web form named `a_name' for node `a_node' (if set), using form action url `a_url', and for type of node `a_node_type'.
+ require
+ is_trashed: attached a_node as l_node and then a_node.is_trashed
+ local
+ f: CMS_FORM
+ ts: WSF_FORM_SUBMIT_INPUT
+ do
+ create f.make (a_url, a_name)
+
+ f.extend_html_text (" ")
+ f.extend_html_text ("Are you sure you want to delete? ")
+
+ -- TODO check if we need to check for has_permissions!!
+ if
+ a_node /= Void and then
+ a_node.id > 0
+ then
+ create ts.make ("op")
+ ts.set_default_value ("Delete")
+ fixme ("[
+ ts.set_default_value (translation ("Delete"))
+ ]")
+ f.extend (ts)
+ create ts.make ("op")
+ ts.set_default_value ("Cancel")
+ ts.set_formaction ("/node/"+a_node.id.out)
+ ts.set_formmethod ("GET")
+ f.extend (ts)
+ end
+ f.extend_html_text (" ")
+ f.extend_html_text ("Do you want to restore the current node? ")
+ if
+ a_node /= Void and then
+ a_node.id > 0
+ then
+ create ts.make ("op")
+ ts.set_default_value ("Restore")
+ ts.set_formaction ("/node/"+a_node.id.out+"/delete")
+ ts.set_formmethod ("POST")
+ fixme ("[
+ ts.set_default_value (translation ("Restore"))
+ ]")
+ f.extend (ts)
+ end
+ Result := f
+ end
+
+
+ new_trash_form (a_node: detachable CMS_NODE; a_url: READABLE_STRING_8; a_name: STRING; a_node_type: CMS_NODE_TYPE [CMS_NODE]): CMS_FORM
+ -- Create a web form named `a_name' for node `a_node' (if set), using form action url `a_url', and for type of node `a_node_type'.
+ local
+ f: CMS_FORM
+ ts: WSF_FORM_SUBMIT_INPUT
+ do
+ create f.make (a_url, a_name)
+
+ f.extend_html_text (" ")
+ f.extend_html_text ("Are you sure you want to trash the current node? ")
+ if
+ a_node /= Void and then
+ a_node.id > 0
+ then
+ create ts.make ("op")
+ ts.set_default_value ("Trash")
+ fixme ("[
+ ts.set_default_value (translation ("Trash"))
+ ]")
+ f.extend (ts)
+ end
+ Result := f
+ end
+
+
+ populate_form (a_content_type: CMS_NODE_TYPE [CMS_NODE]; a_form: WSF_FORM; a_node: detachable CMS_NODE)
+ -- Fill the web form `a_form' with data from `a_node' if set,
+ -- and apply this to content type `a_content_type'.
+ do
+ if attached node_api.node_type_webform_manager (a_content_type) as wf then
+ wf.populate_form (Current, a_form, a_node)
+ end
+ end
+
+ new_node (a_content_type: CMS_NODE_TYPE [CMS_NODE]; a_form_data: WSF_FORM_DATA; a_node: detachable CMS_NODE): CMS_NODE
+ -- Node creation with form_data `a_form_data' for the given content type `a_content_type'
+ -- using optional `a_node' to get extra node data.
+ do
+ if attached node_api.node_type_webform_manager (a_content_type) as wf then
+ Result := wf.new_node (Current, a_form_data, a_node)
+ else
+ Result := a_content_type.new_node (a_node)
+ end
+ Result.set_author (user)
+ end
+
+ apply_form_data_to_node (a_content_type: CMS_NODE_TYPE [CMS_NODE]; a_form_data: WSF_FORM_DATA; a_node: CMS_NODE)
+ -- Update node `a_node' with form_data `a_form_data' for the given content type `a_content_type'.
+ do
+ if attached node_api.node_type_webform_manager (a_content_type) as wf then
+ wf.update_node (Current, a_form_data, a_node)
+ end
+ end
+
+end
diff --git a/modules/node/handler/node_handler.e b/modules/node/handler/node_handler.e
new file mode 100644
index 0000000..516aef2
--- /dev/null
+++ b/modules/node/handler/node_handler.e
@@ -0,0 +1,388 @@
+note
+ description: "[
+ handler for CMS node in the CMS interface.
+
+ TODO: implement REST API.
+ ]"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ NODE_HANDLER
+
+inherit
+ CMS_NODE_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ execute as uri_execute,
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_URI_TEMPLATE_HANDLER
+ rename
+ execute as uri_template_execute,
+ new_mapping as new_uri_template_mapping
+ select
+ new_uri_template_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get,
+ do_post,
+ do_put,
+ do_delete
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+ uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+ uri_template_execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute (req, res)
+ end
+
+feature -- Query
+
+ node_id_path_parameter (req: WSF_REQUEST): INTEGER_64
+ -- Node id passed as path parameter for request `req'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_node: detachable CMS_NODE
+ l_nid, l_rev: INTEGER_64
+ edit_response: NODE_FORM_RESPONSE
+ view_response: NODE_VIEW_RESPONSE
+ do
+ if req.percent_encoded_path_info.ends_with ("/edit") then
+ check valid_url: req.percent_encoded_path_info.starts_with ("/node/") end
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with ("/delete") then
+ check valid_url: req.percent_encoded_path_info.starts_with ("/node/") end
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with ("/trash") then
+ check valid_url: req.percent_encoded_path_info.starts_with ("/node/") end
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with ("/revision") then
+ do_revisions (req, res)
+-- elseif req.percent_encoded_path_info.ends_with ("/add_child/page") then
+-- -- Add child node
+-- l_nid := node_id_path_parameter (req)
+-- if l_nid > 0 then
+-- -- create a new child node with node id `l_id' as parent.
+-- create_new_node (req, res)
+-- else
+-- send_not_found (req, res)
+-- end
+ else
+ -- Display existing node
+ l_nid := node_id_path_parameter (req)
+ if l_nid > 0 then
+ if
+ attached {WSF_STRING} req.query_parameter ("revision") as p_rev and then
+ p_rev.value.is_integer_64
+ then
+ l_rev := p_rev.value.to_integer_64
+ end
+ l_node := node_api.node (l_nid)
+ if
+ l_node /= Void and then
+ l_rev > 0 and then
+ node_api.has_permission_for_action_on_node ("view revisions", l_node, current_user (req))
+ then
+ l_node := node_api.revision_node (l_nid, l_rev)
+ end
+ if l_node = Void then
+ send_not_found (req, res)
+ else
+ if
+ l_rev > 0 or else l_node.is_published
+ then
+ create view_response.make (req, res, api, node_api)
+ view_response.set_node (l_node)
+ view_response.set_revision (l_rev)
+ view_response.execute
+ elseif
+ attached current_user (req) as l_user and then
+ ( node_api.is_author_of_node (l_user, l_node)
+ or else api.user_api.user_has_permission (l_user, "view unpublished " + l_node.content_type)
+ )
+ then
+ create view_response.make (req, res, api, node_api)
+ view_response.set_node (l_node)
+ view_response.set_revision (l_rev)
+ view_response.execute
+ else
+ send_access_denied (req, res)
+ end
+ end
+ else
+-- redirect_to (req.absolute_script_url ("/node/"), res) -- New node.
+-- send_bad_request (req, res)
+ create_new_node (req, res)
+ end
+ end
+ end
+
+ do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ edit_response: NODE_FORM_RESPONSE
+ do
+ fixme ("Refactor code: extract methods: edit_node and add_node")
+ if req.percent_encoded_path_info.ends_with ("/edit") then
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with ("/delete") then
+ if
+ attached {WSF_STRING} req.form_parameter ("op") as l_op and then
+ l_op.value.same_string ("Delete")
+ then
+ do_delete (req, res)
+ elseif
+ attached {WSF_STRING} req.form_parameter ("op") as l_op and then
+ l_op.value.same_string ("Restore")
+ then
+ do_restore (req, res)
+ end
+ elseif req.percent_encoded_path_info.ends_with ("/trash") then
+ if
+ attached {WSF_STRING} req.form_parameter ("op") as l_op and then
+ l_op.value.same_string ("Trash")
+ then
+ do_trash (req, res)
+ end
+ elseif req.percent_encoded_path_info.starts_with ("/node/add/") then
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.percent_encoded_path_info.ends_with ("/add_child/page") then
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ else
+ to_implement ("REST API")
+ send_not_implemented ("REST API not yet implemented", req, res)
+ end
+ end
+
+ do_put (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ do
+ to_implement ("REST API")
+ send_not_implemented ("REST API not yet implemented", req, res)
+ end
+
+ do_trash (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Trash a node, soft delete.
+ do
+ if attached current_user (req) as l_user then
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached node_api.node (l_id.integer_value) as l_node
+ then
+ if node_api.has_permission_for_action_on_node ("trash", l_node, current_user (req)) then
+ node_api.trash_node (l_node)
+ res.send (create {CMS_REDIRECTION_RESPONSE_MESSAGE}.make (req.absolute_script_url ("")))
+ else
+ send_access_denied (req, res)
+ -- send_not_authorized ?
+ end
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ else
+ send_access_denied (req, res)
+ end
+ end
+
+ process_node_creation (req: WSF_REQUEST; res: WSF_RESPONSE; a_user: CMS_USER)
+ do
+ to_implement ("REST API")
+ send_not_implemented ("REST API not yet implemented", req, res)
+ end
+
+feature {NONE} -- Trash:Restore
+
+ do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Delete a node from the database.
+ local
+ l_source: STRING
+ do
+ if attached current_user (req) as l_user then
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached {CMS_NODE} node_api.node (l_id.integer_value) as l_node
+ then
+ if node_api.has_permission_for_action_on_node ("delete", l_node, current_user (req)) then
+ node_api.delete_node (l_node)
+ l_source := node_api.node_path (l_node)
+ api.unset_path_alias (l_source, api.location_alias (l_source))
+ res.send (create {CMS_REDIRECTION_RESPONSE_MESSAGE}.make (req.absolute_script_url ("")))
+ else
+ send_access_denied (req, res)
+ -- send_not_authorized ?
+ end
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ else
+ send_access_denied (req, res)
+ end
+ end
+
+ do_restore (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Restore a node: From {CMS_NODE_API}.trashed to {CMS_NODE_API}.not_published.
+ do
+ if attached current_user (req) as l_user then
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached node_api.node (l_id.integer_value) as l_node
+ then
+ if node_api.has_permission_for_action_on_node ("restore", l_node, current_user (req)) then
+ node_api.restore_node (l_node)
+ res.send (create {CMS_REDIRECTION_RESPONSE_MESSAGE}.make (req.absolute_script_url ("")))
+ else
+ send_access_denied (req, res)
+ -- send_not_authorized ?
+ end
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ else
+ send_access_denied (req, res)
+ end
+ end
+
+ do_revisions (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Display revisions of a node.
+ local
+ r: GENERIC_VIEW_CMS_RESPONSE
+ b: STRING
+ n: CMS_NODE
+ do
+ if attached {WSF_STRING} req.path_parameter ("id") as l_id then
+ if
+ l_id.is_integer and then
+ attached node_api.node (l_id.integer_value) as l_node
+ then
+ if node_api.has_permission_for_action_on_node ("view revisions", l_node, current_user (req)) then
+ create r.make (req, res, api)
+ create b.make_empty
+ b.append ("")
+ r.set_title ("Revisions for " + html_encoded (l_node.title))
+ r.set_main_content (b)
+ r.execute
+ else
+ send_access_denied (req, res)
+ -- send_not_authorized ?
+ end
+ else
+ do_error (req, res, l_id)
+ end
+ else
+ (create {INTERNAL_SERVER_ERROR_CMS_RESPONSE}.make (req, res, api)).execute
+ end
+ end
+
+feature -- Error
+
+ do_error (req: WSF_REQUEST; res: WSF_RESPONSE; a_id: detachable WSF_STRING)
+ -- Handling error.
+ local
+ l_page: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ l_page.set_value (req.absolute_script_url (req.percent_encoded_path_info), "request")
+ if a_id /= Void and then a_id.is_integer then
+ -- resource not found
+ l_page.set_value ("404", "code")
+ l_page.set_status_code (404)
+ else
+ -- bad request
+ l_page.set_value ("400", "code")
+ l_page.set_status_code (400)
+ end
+ l_page.execute
+ end
+
+feature {NONE} -- Node
+
+ create_new_node (req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ edit_response: NODE_FORM_RESPONSE
+ do
+ if req.percent_encoded_path_info.starts_with_general ("/node/") then
+ create edit_response.make (req, res, api, node_api)
+ edit_response.execute
+ elseif req.is_get_request_method then
+ redirect_to (req.absolute_script_url ("/node/"), res)
+ else
+ send_bad_request (req, res)
+ end
+ end
+end
diff --git a/modules/node/handler/node_response.e b/modules/node/handler/node_response.e
new file mode 100644
index 0000000..58ebbce
--- /dev/null
+++ b/modules/node/handler/node_response.e
@@ -0,0 +1,80 @@
+note
+ description: "Generic CMS Response for a CMS NODE."
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ NODE_RESPONSE
+
+inherit
+ CMS_RESPONSE
+ rename
+ make as make_response
+ end
+
+feature {NONE} -- Initialization
+
+ make (req: WSF_REQUEST; res: WSF_RESPONSE; a_api: like api; a_node_api: like node_api)
+ do
+ node_api := a_node_api
+ make_response (req, res, a_api)
+ end
+
+feature -- Access
+
+ node_api: CMS_NODE_API
+ -- Associated node API.
+
+feature -- Helpers
+
+ node_id_path_parameter: INTEGER_64
+ -- Node id passed as path parameter for request `request'.
+ local
+ s: STRING
+ do
+ if attached {WSF_STRING} request.path_parameter ("id") as p_nid then
+ s := p_nid.value
+ if s.is_integer_64 then
+ Result := s.to_integer_64
+ end
+ end
+ end
+
+feature -- Helpers: cms link
+
+ node_local_link (n: CMS_NODE; a_opt_title: detachable READABLE_STRING_GENERAL): CMS_LOCAL_LINK
+ do
+ if attached n.link as lnk then
+ Result := lnk
+ else
+ Result := node_api.node_link (n)
+ end
+ if a_opt_title /= Void and then not Result.title.same_string_general (a_opt_title) then
+ Result := local_link (a_opt_title, Result.location)
+ end
+ end
+
+feature -- Helpers: html link
+
+ node_html_link (n: CMS_NODE; a_opt_title: detachable READABLE_STRING_GENERAL): like link
+ local
+ l_title: detachable READABLE_STRING_GENERAL
+ do
+ if a_opt_title /= Void then
+ l_title := a_opt_title
+ else
+ l_title := n.title
+ end
+ Result := link (l_title, node_api.node_path (n), Void)
+ end
+
+feature -- Helpers: URL
+
+ node_url (n: CMS_NODE): like url
+ require
+ n_with_id: n.has_id
+ do
+ Result := url (node_api.node_link (n).location, Void)
+ end
+
+end
diff --git a/modules/node/handler/node_view_response.e b/modules/node/handler/node_view_response.e
new file mode 100644
index 0000000..64b800b
--- /dev/null
+++ b/modules/node/handler/node_view_response.e
@@ -0,0 +1,71 @@
+note
+ description: "Summary description for {NODE_VIEW_RESPONSE}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ NODE_VIEW_RESPONSE
+
+inherit
+ NODE_RESPONSE
+
+create
+ make
+
+feature -- Access
+
+ node: detachable CMS_NODE
+
+ revision: INTEGER_64
+ -- If not zero, about history version of the node.
+
+feature -- Element change
+
+ set_node (a_node: like node)
+ do
+ node := a_node
+ end
+
+ set_revision (a_rev: like revision)
+ do
+ revision := a_rev
+ end
+
+feature -- Execution
+
+ process
+ -- Computed response message.
+ local
+ nid: INTEGER_64
+ l_node: like node
+ do
+ l_node := node
+ if l_node = Void then
+ nid := node_id_path_parameter
+ if nid > 0 then
+ l_node := node_api.node (nid)
+ end
+ end
+ if l_node /= Void then
+ if
+ attached node_api.node_type_for (l_node) as l_content_type and then
+ attached node_api.node_type_webform_manager (l_content_type) as l_manager
+ then
+ l_manager.append_content_as_html_to_page (l_node, Current)
+ end
+ elseif revision > 0 then
+ set_main_content ("Missing revision node!")
+ else
+ set_main_content ("Missing node!")
+ end
+ if revision > 0 then
+ add_warning_message ("The revisions let you track differences between multiple versions of a post.")
+ end
+ if l_node /= Void and revision > 0 then
+ set_title ("Revision #" + revision.out + " of " + html_encoded (l_node.title))
+ end
+
+
+ end
+
+end
diff --git a/modules/node/handler/nodes_handler.e b/modules/node/handler/nodes_handler.e
new file mode 100644
index 0000000..b129341
--- /dev/null
+++ b/modules/node/handler/nodes_handler.e
@@ -0,0 +1,131 @@
+note
+ description: "Request handler related to /nodes."
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ NODES_HANDLER
+
+inherit
+ CMS_NODE_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_response: CMS_RESPONSE
+ s: STRING
+ n: CMS_NODE
+ lnk: CMS_LOCAL_LINK
+ l_page_helper: CMS_PAGINATION_GENERATOR
+ s_pager: STRING
+ l_count: NATURAL_64
+ l_include_trashed: BOOLEAN
+ do
+ -- At the moment the template are hardcoded, but we can
+ -- get them from the configuration file and load them into
+ -- the setup class.
+
+ l_count := node_api.nodes_count
+
+ create {GENERIC_VIEW_CMS_RESPONSE} l_response.make (req, res, api)
+
+ create s.make_empty
+ if l_count > 1 then
+ l_response.set_title ("Listing " + l_count.out + " nodes")
+ else
+ l_response.set_title ("Listing " + l_count.out + " node")
+ end
+
+ create s_pager.make_empty
+ create l_page_helper.make ("nodes/?page={page}&size={size}", node_api.nodes_count, 25) -- FIXME: Make this default page size a global CMS settings
+ l_page_helper.get_setting_from_request (req)
+ if l_page_helper.has_upper_limit and then l_page_helper.pages_count > 1 then
+ l_page_helper.append_to_html (l_response, s_pager)
+ if l_page_helper.page_size > 25 then
+ s.append (s_pager)
+ end
+ end
+
+ if attached node_api.recent_nodes (create {CMS_DATA_QUERY_PARAMETERS}.make (l_page_helper.current_page_offset, l_page_helper.page_size)) as lst then
+ if attached {WSF_STRING} req.query_parameter ("include_trash") as v and then v.is_case_insensitive_equal ("yes") then
+ l_include_trashed := l_response.has_permissions (<<"view trash", "view any trash">>)
+ end
+ s.append ("%N")
+ across
+ lst as ic
+ loop
+ n := ic.item
+ if not n.is_trashed or else l_include_trashed then
+ lnk := node_api.node_link (n)
+ s.append ("")
+ s.append (l_response.link (lnk.title, lnk.location, Void))
+ if not n.is_published then
+ s.append (" (not-published) ")
+ elseif n.is_trashed then
+ s.append (" (trashed) ")
+ end
+ debug
+ if attached node_api.node_type (n.content_type) as ct then
+ s.append ("")
+ s.append (html_encoded (ct.title))
+ s.append (" ")
+ end
+ end
+ end
+ s.append (" %N")
+ end
+ s.append (" %N")
+ end
+ -- Again the pager at the bottom, if needed
+ s.append (s_pager)
+
+ if l_response.has_permissions (<<"view trash", "view any trash">>) then
+ if not l_include_trashed then
+ s.append (l_response.link ("With trashed items", l_response.location + "?include_trash=yes", Void))
+ s.append (" | ")
+ end
+
+ s.append (l_response.link ("Global-Trash", "trash", Void))
+ s.append (" | ")
+ end
+ if attached l_response.user as u and then l_response.has_permission ("view own trash") then
+ s.append (l_response.link ("Your-trash", "trash?user=" + l_response.url_encoded (u.name), Void))
+ s.append (" | ")
+ end
+
+ l_response.set_main_content (s)
+ l_response.execute
+ end
+
+end
diff --git a/modules/node/handler/trash_handler.e b/modules/node/handler/trash_handler.e
new file mode 100644
index 0000000..31d34ec
--- /dev/null
+++ b/modules/node/handler/trash_handler.e
@@ -0,0 +1,90 @@
+note
+ description: "Request handler related to /trash "
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ TRASH_HANDLER
+
+
+inherit
+ CMS_NODE_HANDLER
+
+ WSF_URI_HANDLER
+ rename
+ new_mapping as new_uri_mapping
+ end
+
+ WSF_RESOURCE_HANDLER_HELPER
+ redefine
+ do_get
+ end
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- execute
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute request handler
+ do
+ execute_methods (req, res)
+ end
+
+feature -- HTTP Methods
+
+ do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
+ --
+ local
+ l_page: CMS_RESPONSE
+ s: STRING
+ n: CMS_NODE
+ lnk: CMS_LOCAL_LINK
+ l_username: detachable READABLE_STRING_32
+ l_trash_owner: detachable CMS_USER
+ do
+ -- At the moment the template is hardcoded, but we can
+ -- get them from the configuration file and load them into
+ -- the setup class.
+ create {GENERIC_VIEW_CMS_RESPONSE} l_page.make (req, res, api)
+ if attached {WSF_STRING} req.query_parameter ("user") as p_username then
+ l_username := p_username.value
+ l_trash_owner := api.user_api.user_by_name (l_username)
+ end
+ if
+ (l_trash_owner /= Void and then l_page.has_permissions (<<"view any trash", "view own trash">>))
+ or else (l_page.has_permission ("view trash"))
+ then
+ -- NOTE: for development purposes we have the following hardcode output.
+ if l_trash_owner /= Void then
+ create s.make_from_string ("Trash for user " + l_page.html_encoded (l_trash_owner.name) + "
")
+ else
+ create s.make_from_string ("Trash
")
+ end
+
+ if attached node_api.trashed_nodes (l_trash_owner) as lst then
+ s.append ("%N")
+ across
+ lst as ic
+ loop
+ n := ic.item
+ lnk := node_api.node_link (n)
+ s.append ("")
+ s.append (l_page.link (lnk.title, lnk.location, Void))
+ s.append (" %N")
+ end
+ s.append (" %N")
+ end
+
+ l_page.set_main_content (s)
+ -- l_page.add_block (create {CMS_CONTENT_BLOCK}.make ("nodes_warning", Void, "/nodes/ is not yet fully implemented ", Void), "highlighted")
+ l_page.execute
+ else
+ create {FORBIDDEN_ERROR_CMS_RESPONSE} l_page.make (req, res, api)
+ l_page.execute
+ end
+ end
+
+end
diff --git a/modules/node/node-safe.ecf b/modules/node/node-safe.ecf
new file mode 100644
index 0000000..00ea6fc
--- /dev/null
+++ b/modules/node/node-safe.ecf
@@ -0,0 +1,29 @@
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/node/node.ecf b/modules/node/node.ecf
new file mode 100644
index 0000000..bfbfead
--- /dev/null
+++ b/modules/node/node.ecf
@@ -0,0 +1,29 @@
+
+
+
+
+
+ /EIFGENs$
+ /CVS$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/node/node_module.e b/modules/node/node_module.e
new file mode 100644
index 0000000..f1bb690
--- /dev/null
+++ b/modules/node/node_module.e
@@ -0,0 +1,18 @@
+note
+ description: "Summary description for {NODE_MODULE}."
+ author: ""
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ NODE_MODULE
+
+obsolete "Use {CMS_NODE_MODULE}"
+
+inherit
+ CMS_NODE_MODULE
+
+create
+ make
+
+end
diff --git a/modules/node/persistence/cms_node_storage_extension.e b/modules/node/persistence/cms_node_storage_extension.e
new file mode 100644
index 0000000..4943ffa
--- /dev/null
+++ b/modules/node/persistence/cms_node_storage_extension.e
@@ -0,0 +1,71 @@
+note
+ description: "Node storage extension for specific node descendant."
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_NODE_STORAGE_EXTENSION [G -> CMS_NODE]
+
+feature -- Change
+
+ set_node_api (a_node_api: CMS_NODE_API)
+ do
+ node_api := a_node_api
+ end
+
+feature -- Access
+
+ node_api: CMS_NODE_API
+
+ content_type: READABLE_STRING_8
+ deferred
+ end
+
+feature -- Status report
+
+ is_accepted (a_node: CMS_NODE): BOOLEAN
+ -- Is `a_node' accepted by current storage extension?
+ do
+ Result := attached {G} a_node
+ end
+
+feature -- Persistence
+
+ store_node (a_node: CMS_NODE)
+ require
+ a_node_accepted: is_accepted (a_node)
+ do
+ if attached {G} a_node as obj then
+ store (obj)
+ end
+ end
+
+ load_node (a_node: CMS_NODE)
+ require
+ a_node_accepted: is_accepted (a_node)
+ do
+ if attached {G} a_node as obj then
+ load (obj)
+ end
+ end
+
+ delete_node (a_node: CMS_NODE)
+ -- remove node extensions.
+ require
+ a_node_accepted: is_accepted (a_node)
+ deferred
+ end
+
+feature {NONE} -- Persistence implementation
+
+ store (a_node: G)
+ -- Store extension data from node `a_node'.
+ deferred
+ end
+
+ load (a_node: G)
+ -- Load extension data into node `a_node'.
+ deferred
+ end
+
+end
diff --git a/modules/node/persistence/cms_node_storage_i.e b/modules/node/persistence/cms_node_storage_i.e
new file mode 100644
index 0000000..dc941c2
--- /dev/null
+++ b/modules/node/persistence/cms_node_storage_i.e
@@ -0,0 +1,254 @@
+note
+ description: "Summary description for {CMS_NODE_STORAGE_I}."
+ date: "$Date: 2015-01-27 19:15:02 +0100 (mar., 27 janv. 2015) $"
+ revision: "$Revision: 96542 $"
+
+deferred class
+ CMS_NODE_STORAGE_I
+
+feature -- Error Handling
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ deferred
+ end
+
+feature -- Storage extension
+
+ register_node_storage_extension (a_extension: CMS_NODE_STORAGE_EXTENSION [CMS_NODE])
+ -- Register `a_extension' as extension to the node storage system.
+ local
+ tb: like node_storage_extensions
+ do
+ tb := node_storage_extensions
+ if tb = Void then
+ create tb.make_caseless (1)
+ node_storage_extensions := tb
+ end
+ tb.force (a_extension, a_extension.content_type)
+ end
+
+ node_storage_extension (a_node: CMS_NODE): detachable CMS_NODE_STORAGE_EXTENSION [CMS_NODE]
+ -- Extension to the node storage system for node `a_node'.
+ do
+ if attached node_storage_extensions as tb then
+ Result := tb.item (a_node.content_type)
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ node_storage_extensions: detachable STRING_TABLE [CMS_NODE_STORAGE_EXTENSION [CMS_NODE]]
+ -- Table of node storage extensions.
+
+ extended_store (a_node: CMS_NODE)
+ -- Store extended data from `a_node'.
+ require
+ not_has_error: not error_handler.has_error
+ do
+ if attached node_storage_extension (a_node) as ext then
+ ext.store_node (a_node)
+ end
+ end
+
+ extended_load (a_node: CMS_NODE)
+ -- Load extended data into `a_node'.
+ require
+ not_has_error: not error_handler.has_error
+ do
+ if attached node_storage_extension (a_node) as ext then
+ ext.load_node (a_node)
+ end
+ end
+
+ extended_delete (a_node: CMS_NODE)
+ -- Delete extended data related to node `a_node'.
+ require
+ not_has_error: not error_handler.has_error
+ do
+ if attached node_storage_extension (a_node) as ext then
+ ext.delete_node (a_node)
+ end
+ end
+
+feature -- Access
+
+ nodes_count: NATURAL_64
+ -- Count of nodes.
+ deferred
+ end
+
+ nodes: LIST [CMS_NODE]
+ -- List of nodes.
+ deferred
+ end
+
+ node_revisions (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- Revisions of node `a_node'.
+ deferred
+ end
+
+ trashed_nodes (a_user: detachable CMS_USER): LIST [CMS_NODE]
+ -- List of nodes by user `a_user' if set, or any.
+ require
+ a_user /= Void implies a_user.has_id
+ deferred
+ end
+
+ recent_nodes (a_lower: INTEGER; a_count: INTEGER): LIST [CMS_NODE]
+ -- List of recent `a_count' nodes with an offset of `lower'.
+ deferred
+ end
+
+ recent_node_changes_before (a_lower: INTEGER; a_count: INTEGER; a_date: DATE_TIME): LIST [CMS_NODE]
+ -- List of recent changes, before `a_date', according to `params' settings.
+ deferred
+ end
+
+ node_by_id (a_id: INTEGER_64): detachable CMS_NODE
+ -- Retrieve node by id `a_id', if any.
+ require
+ a_id > 0
+ deferred
+ end
+
+ node_by_id_and_revision (a_node_id, a_revision: INTEGER_64): detachable CMS_NODE
+ -- Retrieve node by node id `a_node_id' and revision `a_revision', if any.
+ require
+ has_node_id: a_node_id > 0
+ has_revision: a_revision > 0
+ deferred
+ end
+
+ node_author (a_node: CMS_NODE): detachable CMS_USER
+ -- Node's author. if any.
+ require
+ valid_node: a_node.has_id
+ deferred
+ end
+
+ nodes_of_type (a_node_type: CMS_CONTENT_TYPE): LIST [CMS_NODE]
+ -- List of nodes of type `a_node_type'.
+ --| Redefine to optimize!
+ do
+ Result := nodes
+ from
+ Result.start
+ until
+ Result.after
+ loop
+ if Result.item.content_type.same_string (a_node_type.name) then
+ Result.forth
+ else
+ Result.remove
+ end
+ end
+ ensure
+ expected_type: across Result as ic all ic.item.content_type.same_string (a_node_type.name) end
+ end
+
+feature -- Access: outline
+
+ children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
+ -- Children of node `a_node'.
+ -- note: this is the partial version of the nodes.
+ deferred
+ end
+
+ available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- Given the node `a_node', return the list of possible parent nodes id
+ deferred
+ ensure
+ a_node_excluded: across Result as ic all not a_node.same_node (ic.item) end
+ end
+
+feature -- Change: Node
+
+ save_node (a_node: CMS_NODE)
+ -- Create or update `a_node'.
+ do
+ if a_node.has_id then
+ update_node (a_node)
+ else
+ new_node (a_node)
+ end
+ end
+
+ new_node (a_node: CMS_NODE)
+ -- Save node `a_node'.
+ require
+ no_id: not a_node.has_id
+ valid_user: attached a_node.author as l_author and then l_author.id > 0
+ deferred
+ ensure
+ has_id: not error_handler.has_error implies a_node.has_id
+ end
+
+ update_node (a_node: CMS_NODE)
+ -- Update node content `a_node'.
+ -- The user `a_id' is an existing or new collaborator.
+ require
+ has_id: a_node.has_id
+ has_author: attached a_node.author as l_author and then l_author.has_id
+ deferred
+ end
+
+ delete_node (a_node: CMS_NODE)
+ -- Delete `a_node'.
+ require
+ valid_node_id: a_node.has_id
+ do
+ -- TODO
+ -- Check if we need to use a transaction
+ -- we delete a node
+ -- node_revisions
+ -- and extensions (PAGE, BLOG, etc).
+ delete_node_base (a_node)
+ end
+
+ delete_node_base (a_node: CMS_NODE)
+ -- Remove node `a_node'.
+ deferred
+ end
+
+ trash_node (a_node: CMS_NODE)
+ -- Trash `a_node'.
+ do
+ if a_node.has_id then
+ trash_node_by_id (a_node.id)
+ end
+ end
+
+ restore_node (a_node: CMS_NODE)
+ -- Restore `a_node'.
+ do
+ if a_node.has_id then
+ restore_node_by_id (a_node.id)
+ end
+ end
+
+ trash_node_by_id (a_id: INTEGER_64)
+ -- Trash node by id `a_id'.
+ require
+ valid_node_id: a_id > 0
+ deferred
+ end
+
+ restore_node_by_id (a_id: INTEGER_64)
+ -- Restore node by id `a_id'.
+ require
+ valid_node_id: a_id > 0
+ deferred
+ end
+
+feature -- Helpers
+
+ fill_node (a_node: CMS_NODE)
+ -- Fill `a_node' with extra information from database.
+ -- i.e: specific to each content type data.
+ require
+ has_id: a_node.has_id
+ deferred
+ end
+
+end
diff --git a/modules/node/persistence/cms_node_storage_null.e b/modules/node/persistence/cms_node_storage_null.e
new file mode 100644
index 0000000..34cf6a3
--- /dev/null
+++ b/modules/node/persistence/cms_node_storage_null.e
@@ -0,0 +1,152 @@
+note
+ description: "[
+ Objects that ...
+ ]"
+ author: "$Author$"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_NODE_STORAGE_NULL
+
+inherit
+ CMS_NODE_STORAGE_I
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Initialize `Current'.
+ do
+ create error_handler.make
+ end
+
+feature -- Error Handling
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+
+feature -- Access: node
+
+ nodes_count: NATURAL_64
+ -- Count of nodes.
+ do
+ end
+
+ nodes: LIST [CMS_NODE]
+ -- List of nodes.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ node_revisions (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- Revisions of node `a_node'.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ trashed_nodes (a_user: detachable CMS_USER): LIST [CMS_NODE]
+ -- .
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ recent_nodes (a_lower: INTEGER; a_count: INTEGER): LIST [CMS_NODE]
+ -- List of the `a_count' most recent nodes, starting from `a_lower'.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ recent_node_changes_before (a_lower: INTEGER; a_count: INTEGER; a_date: DATE_TIME): LIST [CMS_NODE]
+ -- List of recent changes, before `a_date', according to `params' settings.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+ node_by_id (a_id: INTEGER_64): detachable CMS_NODE
+ --
+ do
+ end
+
+ node_by_id_and_revision (a_node_id, a_revision: INTEGER_64): detachable CMS_NODE
+ --
+ do
+ end
+
+ node_author (a_node: CMS_NODE): detachable CMS_USER
+ -- Node's author. if any.
+ do
+ end
+
+ node_collaborators (a_id: like {CMS_NODE}.id): LIST [CMS_USER]
+ -- Possible list of node's collaborator.
+ do
+ create {ARRAYED_LIST [CMS_USER]} Result.make (0)
+ end
+
+feature -- Access: outline
+
+ children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
+ --
+ do
+ end
+
+ available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
+ --
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ end
+
+feature -- Node
+
+ new_node (a_node: CMS_NODE)
+ -- Add a new node
+ do
+ end
+
+ delete_node_base (a_node: CMS_NODE)
+ --
+ do
+ end
+
+ update_node (a_node: CMS_NODE)
+ --
+ do
+ end
+
+ trash_node_by_id (a_id: INTEGER_64)
+ --
+ do
+ end
+
+ restore_node_by_id (a_id: INTEGER_64)
+ --
+ do
+ end
+
+-- update_node_title (a_user_id: like {CMS_NODE}.id; a_node_id: like {CMS_NODE}.id; a_title: READABLE_STRING_32)
+-- --
+-- do
+-- end
+
+-- update_node_summary (a_user_id: like {CMS_NODE}.id; a_node_id: like {CMS_NODE}.id; a_summary: READABLE_STRING_32)
+-- --
+-- do
+-- end
+
+-- update_node_content (a_user_id: like {CMS_NODE}.id; a_node_id: like {CMS_NODE}.id; a_content: READABLE_STRING_32)
+-- --
+-- do
+-- end
+
+feature -- Helpers
+
+ fill_node (a_node: CMS_NODE)
+ -- Fill `a_node' with extra information from database.
+ -- i.e: specific to each content type data.
+ do
+ end
+
+end
diff --git a/modules/node/persistence/cms_node_storage_sql.e b/modules/node/persistence/cms_node_storage_sql.e
new file mode 100644
index 0000000..3047652
--- /dev/null
+++ b/modules/node/persistence/cms_node_storage_sql.e
@@ -0,0 +1,660 @@
+note
+ description: "[
+ CMS NODE Storage based on SQL statements.
+ ]"
+ date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
+ revision: "$Revision: 96616 $"
+
+class
+ CMS_NODE_STORAGE_SQL
+
+inherit
+ CMS_PROXY_STORAGE_SQL
+
+ CMS_NODE_STORAGE_I
+ redefine
+ nodes_of_type
+ end
+
+ CMS_STORAGE_SQL_I
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Access
+
+ nodes_count: NATURAL_64
+ -- Number of items nodes.
+ do
+ error_handler.reset
+ write_information_log (generator + ".nodes_count")
+ sql_query (sql_select_nodes_count, Void)
+ if not has_error and not sql_after then
+ Result := sql_read_natural_64 (1)
+ end
+ sql_finalize
+ end
+
+ nodes: LIST [CMS_NODE]
+ -- List of nodes.
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".nodes")
+
+ from
+ sql_query (sql_select_nodes, Void)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ node_revisions (a_node: CMS_NODE): LIST [CMS_NODE]
+ -- Revisions of node `a_node'.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+ Result.force (a_node)
+
+ error_handler.reset
+ write_information_log (generator + ".node_revisions")
+
+ from
+ create l_parameters.make (1)
+ l_parameters.force (a_node.id, "nid")
+ l_parameters.force (a_node.revision, "revision")
+ sql_query (sql_select_node_revisions, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ trashed_nodes (a_user: detachable CMS_USER): LIST [CMS_NODE]
+ -- List of nodes.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".trashed_nodes")
+
+ from
+ create l_parameters.make (1)
+ if a_user /= Void and then a_user.has_id then
+ l_parameters.put (a_user.id, "author")
+ sql_query (sql_select_trash_nodes_by_author, l_parameters)
+ else
+ sql_query (sql_select_trash_nodes, Void)
+ end
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ recent_nodes (a_lower: INTEGER; a_count: INTEGER): LIST [CMS_NODE]
+ -- List of recent `a_count' nodes with an offset of `lower'.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".recent_nodes")
+
+ from
+ create l_parameters.make (2)
+ l_parameters.put (a_count, "size")
+ l_parameters.put (a_lower, "offset")
+ sql_query (sql_select_recent_nodes, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ recent_node_changes_before (a_lower: INTEGER; a_count: INTEGER; a_date: DATE_TIME): LIST [CMS_NODE]
+ -- List of recent changes, before `a_date', according to `params' settings.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".recent_node_changes_before")
+
+ from
+ create l_parameters.make (3)
+ l_parameters.put (a_count, "size")
+ l_parameters.put (a_lower, "offset")
+ l_parameters.put (a_date, "date")
+
+ sql_query (sql_select_recent_node_changes_before, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ node_by_id (a_id: INTEGER_64): detachable CMS_NODE
+ -- Retrieve node by id `a_id', if any.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".node_by_id")
+ create l_parameters.make (1)
+ l_parameters.put (a_id, "nid")
+ sql_query (sql_select_node_by_id, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_node
+ end
+ sql_finalize
+ end
+
+ node_by_id_and_revision (a_node_id, a_revision: INTEGER_64): detachable CMS_NODE
+ -- Retrieve node by id `a_id', if any.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".node_by_id_and_revision")
+ create l_parameters.make (1)
+ l_parameters.put (a_node_id, "nid")
+ l_parameters.put (a_revision, "revision")
+ sql_query (sql_select_node_by_id_and_revision, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_node
+ end
+ sql_finalize
+ end
+
+ node_author (a_node: CMS_NODE): detachable CMS_USER
+ -- Node's author for the given node id.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".node_author")
+ create l_parameters.make (2)
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ sql_query (Select_user_author, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_author
+ end
+ sql_finalize
+ end
+
+ last_inserted_node_id: INTEGER_64
+ -- Last insert node id.
+ do
+ error_handler.reset
+ write_information_log (generator + ".last_inserted_node_id")
+ sql_query (Sql_last_insert_node_id, Void)
+ if not has_error and not sql_after then
+ Result := sql_read_integer_64 (1)
+ end
+ sql_finalize
+ end
+
+ last_inserted_node_revision (a_node: detachable CMS_NODE): INTEGER_64
+ -- Last insert revision for node of id `nid'.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".last_inserted_node_revision")
+ if a_node /= Void and then a_node.has_id then
+ create l_parameters.make (1)
+ l_parameters.force (a_node.id, "nid")
+ sql_query (Sql_last_insert_node_revision_for_nid, l_parameters)
+ if not has_error and not sql_after then
+ if sql_item (1) /= Void then
+ Result := sql_read_integer_64 (1)
+ end
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ end
+ end
+ sql_finalize
+ end
+-- if Result = 0 and not has_error then --| include the case a_node = Void
+-- sql_query (Sql_last_insert_node_revision, Void)
+-- if not has_error and not sql_after then
+-- if sql_item (1) /= Void then
+-- Result := sql_read_integer_64 (1)
+-- end
+-- end
+-- end
+ end
+
+ nodes_of_type (a_node_type: CMS_CONTENT_TYPE): LIST [CMS_NODE]
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".nodes_of_type")
+ create l_parameters.make (1)
+ l_parameters.put (a_node_type.name, "node_type")
+
+ from
+ sql_query (sql_select_nodes_of_type, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ check expected_node_type: l_node.content_type.same_string (a_node_type.name) end
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+feature -- Access: outline
+
+ children (a_node: CMS_NODE): detachable LIST [CMS_NODE]
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".children")
+
+ from
+ create l_parameters.make (1)
+ l_parameters.put (a_node.id, "nid")
+ sql_query (sql_select_children_of_node, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+ available_parents_for_node (a_node: CMS_NODE): LIST [CMS_NODE]
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ create {ARRAYED_LIST [CMS_NODE]} Result.make (0)
+
+ error_handler.reset
+ write_information_log (generator + ".available_parents_for_node")
+
+ from
+ create l_parameters.make (1)
+ l_parameters.put (a_node.id, "nid")
+ sql_query (sql_select_available_parents_for_node, l_parameters)
+ sql_start
+ until
+ sql_after
+ loop
+ if attached fetch_node as l_node then
+ Result.force (l_node)
+ end
+ sql_forth
+ end
+ sql_finalize
+ end
+
+feature -- Change: Node
+
+ new_node (a_node: CMS_NODE)
+ -- Save node `a_node'.
+ do
+ store_node (a_node)
+ end
+
+ update_node (a_node: CMS_NODE)
+ -- Update node content `a_node'.
+ do
+ store_node (a_node)
+ end
+
+ trash_node_by_id (a_id: INTEGER_64)
+ -- Remove node by id `a_id'.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ write_information_log (generator + ".trash_node_by_id {" + a_id.out + "}")
+
+ error_handler.reset
+ create l_parameters.make (3)
+ l_parameters.put (create {DATE_TIME}.make_now_utc, "changed")
+ l_parameters.put ({CMS_NODE_API}.trashed, "status")
+ l_parameters.put (a_id, "nid")
+ sql_modify (sql_trash_node, l_parameters)
+ sql_finalize
+ end
+
+ delete_node_base (a_node: CMS_NODE)
+ --
+ local
+ l_parameters: STRING_TABLE [ANY]
+ l_time: DATE_TIME
+ do
+ sql_begin_transaction
+ create l_time.make_now_utc
+ write_information_log (generator + ".delete_node_base {" + a_node.id.out + "}")
+
+ error_handler.reset
+ create l_parameters.make (1)
+ l_parameters.put (a_node.id, "nid")
+ sql_modify (sql_delete_node, l_parameters)
+ sql_finalize
+
+ -- we remove node_revisions and pages.
+ -- Check: maybe we need a transaction.
+ sql_modify (sql_delete_node_revisions, l_parameters)
+ sql_finalize
+
+ if not error_handler.has_error then
+ extended_delete (a_node)
+ sql_commit_transaction
+ else
+ sql_rollback_transaction
+ end
+ end
+
+ restore_node_by_id (a_id: INTEGER_64)
+ --
+ local
+ l_parameters: STRING_TABLE [ANY]
+ l_time: DATE_TIME
+ do
+ create l_time.make_now_utc
+ write_information_log (generator + ".restore_node_by_id {" + a_id.out + "}")
+
+ error_handler.reset
+ create l_parameters.make (1)
+ l_parameters.put (l_time, "changed")
+ l_parameters.put ({CMS_NODE_API}.published, "status")
+ l_parameters.put (a_id, "nid")
+ sql_modify (sql_update_node_status, l_parameters)
+ sql_finalize
+ end
+
+
+feature {NONE} -- Implementation
+
+ store_node (a_node: CMS_NODE)
+ local
+ l_copy_parameters: STRING_TABLE [detachable ANY]
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_rev: like last_inserted_node_revision
+ now: DATE_TIME
+ do
+ create now.make_now_utc
+ error_handler.reset
+
+ write_information_log (generator + ".store_node")
+ create l_parameters.make (9)
+ l_parameters.put (a_node.content_type, "type")
+ l_parameters.put (a_node.title, "title")
+ l_parameters.put (a_node.summary, "summary")
+ l_parameters.put (a_node.content, "content")
+ l_parameters.put (a_node.format, "format")
+ l_parameters.put (a_node.publication_date, "publish")
+ l_parameters.put (now, "changed")
+ l_parameters.put (a_node.status, "status")
+ if attached a_node.author as l_author then
+ check valid_author: l_author.has_id end
+ l_parameters.put (l_author.id, "author")
+ else
+ l_parameters.put (0, "author")
+ end
+ sql_begin_transaction
+
+ if a_node.has_id then
+ l_rev := a_node.revision.max (last_inserted_node_revision (a_node)) + 1 --| starts at (nid, 1)
+ else
+ l_rev := last_inserted_node_revision (a_node) + 1 --| starts at (nid, 1)
+ end
+ if a_node.has_id then
+ -- Copy existing node data to node_revisions table.
+ create l_copy_parameters.make (2)
+ l_copy_parameters.force (a_node.id, "nid")
+-- l_copy_parameters.force (l_rev - 1, "revision")
+ sql_insert (sql_copy_node_to_revision, l_copy_parameters)
+ sql_finalize
+
+
+ if not has_error then
+ a_node.set_revision (l_rev)
+
+ -- Update
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ sql_modify (sql_update_node, l_parameters)
+ sql_finalize
+
+ if not error_handler.has_error then
+ a_node.set_modification_date (now)
+ end
+ end
+ else
+ -- Store new node
+ l_parameters.put (a_node.creation_date, "created")
+ l_parameters.put (l_rev, "revision")
+
+ sql_insert (sql_insert_node, l_parameters)
+ sql_finalize
+
+ if not error_handler.has_error then
+ a_node.set_modification_date (now)
+ a_node.set_id (last_inserted_node_id)
+ a_node.set_revision (l_rev) -- New object.
+-- check a_node.revision = last_inserted_node_revision (a_node) end
+ end
+ end
+ if not error_handler.has_error then
+ extended_store (a_node) -- Note, `a_node.revision' is updated.
+ end
+ if error_handler.has_error then
+ sql_rollback_transaction
+ else
+ sql_commit_transaction
+ end
+ end
+
+feature -- Helpers
+
+ fill_node (a_node: CMS_NODE)
+ -- Fill `a_node' with extra information from database.
+ -- i.e: specific to each content type data.
+ do
+ error_handler.reset
+ write_information_log (generator + ".fill_node")
+ extended_load (a_node)
+ end
+
+feature {NONE} -- Queries
+
+ sql_select_nodes_count: STRING = "SELECT count(*) FROM nodes WHERE status != -1 ;"
+ -- Nodes count (Published and not Published)
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_nodes: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE status != -1 ;"
+ -- SQL Query to retrieve all nodes.
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_nodes_of_type: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE status != -1 AND type=:node_type ;"
+ -- SQL Query to retrieve all nodes of type :node_type.
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_node_revisions: STRING = "SELECT nodes.nid, node_revisions.revision, nodes.type, node_revisions.title, node_revisions.summary, node_revisions.content, node_revisions.format, node_revisions.author, nodes.publish, nodes.created, node_revisions.changed, node_revisions.status FROM nodes INNER JOIN node_revisions ON nodes.nid = node_revisions.nid WHERE nodes.nid = :nid AND node_revisions.revision < :revision ORDER BY node_revisions.revision DESC;"
+ -- SQL query to get node revisions (missing the latest one).
+
+ sql_select_trash_nodes: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE status = -1 ;"
+ -- SQL Query to retrieve all trahsed nodes.
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_trash_nodes_by_author: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE status = -1 and author = :author ;"
+ -- SQL Query to retrieve all nodes by a given author.
+ --| note: {CMS_NODE_API}.trashed = -1
+
+ sql_select_node_by_id: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE nid =:nid ORDER BY revision DESC, publish DESC LIMIT 1;"
+
+ sql_select_node_by_id_and_revision: STRING = "SELECT nodes.nid, node_revisions.revision, nodes.type, node_revisions.title, node_revisions.summary, node_revisions.content, node_revisions.format, node_revisions.author, nodes.publish, nodes.created, node_revisions.changed, node_revisions.status FROM nodes INNER JOIN node_revisions ON nodes.nid = node_revisions.nid WHERE nodes.nid = :nid AND node_revisions.revision = :revision ORDER BY node_revisions.revision DESC;"
+
+ sql_select_recent_nodes: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes ORDER BY nid DESC, publish DESC LIMIT :size OFFSET :offset ;"
+
+ sql_select_recent_node_changes_before: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed, status FROM nodes WHERE changed <= :date ORDER BY changed DESC, nid DESC LIMIT :size OFFSET :offset ;"
+
+ sql_insert_node: STRING = "INSERT INTO nodes (revision, type, title, summary, content, format, publish, created, changed, status, author) VALUES (:revision, :type, :title, :summary, :content, :format, :publish, :created, :changed, :status, :author);"
+ -- SQL Insert to add a new node.
+
+ sql_update_node : STRING = "UPDATE nodes SET revision=:revision, type=:type, title=:title, summary=:summary, content=:content, format=:format, publish=:publish, changed=:changed, status=:status, author=:author WHERE nid=:nid;"
+ -- SQL update node.
+
+ sql_trash_node: STRING = "UPDATE nodes SET changed=:changed, status =:status WHERE nid=:nid"
+ -- Soft deletion with free metadata.
+
+ sql_delete_node: STRING = "DELETE FROM nodes WHERE nid=:nid"
+ -- Physical deletion with free metadata.
+
+ sql_update_node_status: STRING = "UPDATE nodes SET changed=:changed, status =:status WHERE nid=:nid"
+ -- Restore node to {CMS_NODE_API}.published
+
+ sql_last_insert_node_id: STRING = "SELECT MAX(nid) FROM nodes;"
+
+ sql_copy_node_to_revision: STRING = "INSERT INTO node_revisions (nid, revision, title, summary, content, format, author, changed, status) SELECT nid, revision, title, summary, content, format, author, changed, status FROM nodes WHERE nid=:nid;"
+
+ Sql_last_insert_node_revision: STRING = "SELECT MAX(revision) FROM node_revisions;"
+ Sql_last_insert_node_revision_for_nid: STRING = "SELECT MAX(revision) FROM node_revisions WHERE nid=:nid;"
+
+ sql_select_available_parents_for_node : STRING = "[
+ SELECT node.nid, node.revision, node.type, title, summary, content, format, author, publish, created, changed, status
+ FROM nodes node LEFT JOIN page_nodes pn ON node.nid = pn.nid AND node.nid != :nid
+ WHERE node.nid != :nid AND pn.parent != :nid AND node.status != -1 GROUP BY node.nid, node.revision;
+ ]"
+
+ sql_select_children_of_node: STRING = "[
+ SELECT node.nid, node.revision, node.type, title, summary, content, format, author, publish, created, changed, status
+ FROM nodes node LEFT JOIN page_nodes pn ON node.nid = pn.nid
+ WHERE pn.parent = :nid AND node.status != -1 GROUP BY node.nid, node.revision;
+ ]"
+
+ sql_delete_node_revisions: STRING = "DELETE FROM node_revisions WHERE nid=:nid;"
+
+
+feature {NONE} -- Sql Queries: USER_ROLES collaborators, author
+
+ Select_user_author: STRING = "SELECT uid, name, password, salt, email, users.status, users.created, signed FROM nodes INNER JOIN users ON nodes.author=users.uid AND nodes.nid = :nid AND nodes.revision = :revision;"
+
+-- Select_node_author: STRING = "SELECT nid, revision, type, title, summary, content, format, author, publish, created, changed FROM users INNER JOIN nodes ON nodes.author=users.uid AND nodes.nid =:nid;"
+
+feature {NONE} -- Implementation
+
+ fetch_node: detachable CMS_PARTIAL_NODE
+ do
+ if attached sql_read_string (3) as l_type then
+ create Result.make_empty (l_type)
+
+ if attached sql_read_integer_64 (1) as l_id then
+ Result.set_id (l_id)
+ end
+ if attached sql_read_integer_64 (2) as l_revision then
+ Result.set_revision (l_revision)
+ end
+ if attached sql_read_string_32 (4) as l_title then
+ Result.set_title (l_title)
+ end
+ if attached sql_read_string_32 (5) as l_summary then
+ Result.set_summary (l_summary)
+ end
+ if attached sql_read_string_32 (6) as l_content then
+ Result.set_content (l_content)
+ end
+ if attached sql_read_string (7) as l_format then
+ Result.set_format (l_format)
+ end
+ if attached sql_read_integer_64 (8) as l_author_id then
+ Result.set_author (create {CMS_PARTIAL_USER}.make_with_id (l_author_id))
+ end
+ if attached sql_read_date_time (9) as l_publication_date then
+ Result.set_publication_date (l_publication_date)
+ end
+ if attached sql_read_date_time (10) as l_creation_date then
+ Result.set_creation_date (l_creation_date)
+ end
+ if attached sql_read_date_time (11) as l_modif_date then
+ Result.set_modification_date (l_modif_date)
+ end
+ if attached sql_read_integer_32 (12) as l_status then
+ Result.set_status (l_status)
+ end
+ end
+ end
+
+ fetch_author: detachable CMS_USER
+ do
+ if attached sql_read_string_32 (2) as l_name and then not l_name.is_whitespace then
+ create Result.make (l_name)
+ if attached sql_read_integer_32 (1) as l_id then
+ Result.set_id (l_id)
+ end
+ if attached sql_read_string (3) as l_password then
+ -- FIXME: should we return the password here ???
+ Result.set_hashed_password (l_password)
+ end
+ if attached sql_read_string (5) as l_email then
+ Result.set_email (l_email)
+ end
+ else
+ check expected_valid_user: False end
+ end
+ end
+
+end
diff --git a/modules/node/persistence/cms_node_storage_sql_page_extension.e b/modules/node/persistence/cms_node_storage_sql_page_extension.e
new file mode 100644
index 0000000..cd0dd5e
--- /dev/null
+++ b/modules/node/persistence/cms_node_storage_sql_page_extension.e
@@ -0,0 +1,168 @@
+note
+ description: "Storage extension for Page nodes."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_NODE_STORAGE_SQL_PAGE_EXTENSION
+
+inherit
+ CMS_NODE_STORAGE_EXTENSION [CMS_PAGE]
+
+ CMS_PROXY_STORAGE_SQL
+ rename
+ make as make_proxy,
+ sql_storage as node_storage
+ redefine
+ node_storage
+ end
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_node_api: CMS_NODE_API; a_sql_storage: CMS_NODE_STORAGE_SQL)
+ do
+ set_node_api (a_node_api)
+ make_proxy (a_sql_storage)
+ end
+
+ node_storage: CMS_NODE_STORAGE_SQL
+ --
+
+feature -- Access
+
+ content_type: STRING
+ once
+ Result := {CMS_PAGE_NODE_TYPE}.name
+ end
+
+feature -- Persistence
+
+ store (a_node: CMS_PAGE)
+ -- .
+ local
+ l_parameters: STRING_TABLE [ANY]
+ l_new_parent_id, l_previous_parent_id: INTEGER_64
+ l_update: BOOLEAN
+ l_has_modif: BOOLEAN
+ do
+ if attached api as l_api then
+ l_api.logger.put_information (generator + ".store", Void)
+ end
+
+ error_handler.reset
+ -- Check existing record, if any.
+ if attached node_data (a_node) as d then
+ l_update := a_node.revision = d.revision
+ l_previous_parent_id := d.parent_id
+ end
+ if not has_error then
+ if attached a_node.parent as l_parent then
+ l_new_parent_id := l_parent.id
+ else
+ l_new_parent_id := 0
+ end
+ l_has_modif := l_has_modif or (l_new_parent_id /= l_previous_parent_id)
+
+ create l_parameters.make (3)
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ l_parameters.force (l_new_parent_id, "parent")
+
+ if l_update then
+ if l_has_modif then
+ sql_modify (sql_update_node_data, l_parameters)
+ end
+ else
+ if l_has_modif then
+ sql_insert (sql_insert_node_data, l_parameters)
+ else
+ -- no page data, means everything is empty.
+ -- FOR NOW: always record row
+ sql_insert (sql_insert_node_data, l_parameters)
+ end
+ end
+ sql_finalize
+ end
+ end
+
+ load (a_node: CMS_PAGE)
+ -- .
+ local
+ ct: CMS_PAGE_NODE_TYPE
+ l_parent_id: INTEGER_64
+ do
+ if attached node_data (a_node) as d then
+ l_parent_id := d.parent_id
+ if
+ l_parent_id > 0 and then
+ l_parent_id /= a_node.id and then
+ attached node_storage.node_by_id (l_parent_id) as l_parent
+ then
+ if attached {CMS_PAGE_NODE_TYPE} node_api.node_type (l_parent.content_type) as l_parent_ct then
+ ct := l_parent_ct
+ else
+ create ct
+ end
+ a_node.set_parent (ct.new_node (l_parent))
+ else
+ write_debug_log ("Invalid parent node id!")
+ end
+ end
+ end
+
+
+ delete_node (a_node: CMS_PAGE)
+ --
+ local
+ l_parameters: STRING_TABLE [ANY]
+ do
+ if a_node.has_id then
+ create l_parameters.make (1)
+ l_parameters.put (a_node.id, "nid")
+ sql_modify (sql_delete_node_data, l_parameters)
+ sql_finalize
+ end
+ end
+
+feature {NONE} -- Implementation
+
+ node_data (a_node: CMS_NODE): detachable TUPLE [revision: INTEGER_64; parent_id: INTEGER_64]
+ -- Node extension data for node `a_node' as tuple.
+ local
+ l_parameters: STRING_TABLE [ANY]
+ n: INTEGER
+ do
+ error_handler.reset
+ create l_parameters.make (2)
+ l_parameters.put (a_node.id, "nid")
+ l_parameters.put (a_node.revision, "revision")
+ sql_query (sql_select_node_data, l_parameters)
+ if not has_error then
+ if not sql_after then
+ -- nid, revision, parent
+ Result := [sql_read_integer_64 (2), sql_read_integer_64 (3)]
+ sql_forth
+ if not sql_after then
+ check unique_data: n = 0 end
+ Result := Void
+ end
+ else
+ check unique_data: n = 0 end
+ end
+ end
+ sql_finalize
+ ensure
+ accepted_revision: Result /= Void implies Result.revision <= a_node.revision
+ end
+
+feature -- SQL
+
+ sql_select_node_data: STRING = "SELECT nid, revision, parent FROM page_nodes WHERE nid=:nid AND revision<=:revision ORDER BY revision DESC LIMIT 1;"
+ sql_insert_node_data: STRING = "INSERT INTO page_nodes (nid, revision, parent) VALUES (:nid, :revision, :parent);"
+ sql_update_node_data: STRING = "UPDATE page_nodes SET nid=:nid, revision=:revision, parent=:parent WHERE nid=:nid AND revision=:revision;"
+ sql_delete_node_data: STRING = "DELETE FROM page_nodes WHERE nid=:nid;"
+
+end
diff --git a/modules/node/site/files/css/node.css b/modules/node/site/files/css/node.css
new file mode 100644
index 0000000..f4277ee
--- /dev/null
+++ b/modules/node/site/files/css/node.css
@@ -0,0 +1,17 @@
+ul.cms-nodes {
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc;
+}
+ul.cms-nodes li {
+ border-top: dotted 1px #ccc;
+}
+ul.cms-nodes li:first-child {
+ border-top: none;
+}
+ul.cms-nodes li.cms_type_page a::before {
+ content: "[page] ";
+}
+ul.cms-nodes li.cms_type_blog a::before {
+ content: "[blog] ";
+}
diff --git a/modules/node/site/files/scss/node.scss b/modules/node/site/files/scss/node.scss
new file mode 100644
index 0000000..5cf9324
--- /dev/null
+++ b/modules/node/site/files/scss/node.scss
@@ -0,0 +1,24 @@
+ul.cms-nodes {
+
+ list-style-type: none;
+ padding: 3px 3px 3px 3px;
+ border: solid 1px #ccc;
+
+ li{
+ border-top: dotted 1px #ccc;
+ &:first-child {
+ border-top: none;
+ }
+ }
+
+ li.cms_type_page a::before {
+ content: "[page] ";
+ }
+
+ li.cms_type_blog a::before {
+ content: "[blog] ";
+ }
+
+
+}
+
diff --git a/modules/node/site/scripts/node.sql b/modules/node/site/scripts/node.sql
new file mode 100644
index 0000000..bdea9a7
--- /dev/null
+++ b/modules/node/site/scripts/node.sql
@@ -0,0 +1,37 @@
+
+CREATE TABLE nodes (
+ `nid` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT UNIQUE,
+ `revision` INTEGER,
+ `type` TEXT NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `summary` TEXT,
+ `content` TEXT,
+ `format` VARCHAR(128),
+ `author` INTEGER,
+ `publish` DATETIME,
+ `created` DATETIME NOT NULL,
+ `changed` DATETIME NOT NULL,
+ `status` INTEGER,
+ CONSTRAINT Unique_nid_revision UNIQUE (nid,revision)
+);
+
+CREATE TABLE node_revisions (
+ `nid` INTEGER NOT NULL,
+ `revision` INTEGER NOT NULL,
+ `title` VARCHAR(255) NOT NULL,
+ `summary` TEXT,
+ `content` TEXT,
+ `format` VARCHAR(128),
+ `author` INTEGER,
+ `changed` DATETIME NOT NULL,
+ `status` INTEGER,
+ CONSTRAINT Unique_nid_revision PRIMARY KEY (nid,revision)
+);
+
+CREATE TABLE page_nodes(
+ `nid` INTEGER NOT NULL,
+ `revision` INTEGER NOT NULL,
+ `parent` INTEGER,
+ CONSTRAINT PK_nid_revision PRIMARY KEY (nid,revision)
+);
+
diff --git a/modules/oauth20/cms_oauth_20_api.e b/modules/oauth20/cms_oauth_20_api.e
new file mode 100644
index 0000000..8952e2e
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_api.e
@@ -0,0 +1,112 @@
+note
+ description: "[
+ API to manage CMS User OAuth authentication.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_API
+
+inherit
+ CMS_MODULE_API
+
+ REFACTORING_HELPER
+
+create {CMS_OAUTH_20_MODULE}
+ make_with_storage
+
+feature {NONE} -- Initialization
+
+ make_with_storage (a_api: CMS_API; a_oauth_storage: CMS_OAUTH_20_STORAGE_I)
+ -- Create an object with api `a_api' and storage `a_oauth_storage'.
+ do
+ oauth_20_storage := a_oauth_storage
+ make (a_api)
+ ensure
+ oauht_20_storage_set: oauth_20_storage = a_oauth_storage
+ end
+
+feature {CMS_MODULE} -- Access: User oauth storage.
+
+ oauth_20_storage: CMS_OAUTH_20_STORAGE_I
+ -- storage interface.
+
+feature -- Access: User Oauth20
+
+ user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by id `a_uid' for the consumer `a_consumer', if any.
+ do
+ Result := oauth_20_storage.user_oauth2_by_id (a_uid, a_consumer)
+ end
+
+ user_oauth2_by_email (a_email: like {CMS_USER}.email; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by email `a_email' for the consumer `a_consumer', if any.
+ do
+ Result := oauth_20_storage.user_oauth2_by_email (a_email, a_consumer)
+ end
+
+ user_oauth2_by_token (a_token: READABLE_STRING_GENERAL; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by token `a_token' for the consumer `a_consumer'.
+ do
+ Result := oauth_20_storage.user_oauth2_by_token (a_token, a_consumer)
+ end
+
+ user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve user by token `a_token' searching in all the registered consumers in the system.
+ do
+ Result := oauth_20_storage.user_oauth2_without_consumer_by_token (a_token)
+ end
+
+feature -- Access: Consumers OAuth20
+
+ oauth2_consumers: LIST [STRING]
+ -- List of Oauth_20 consumers, if any, empty in other case.
+ do
+ Result := oauth_20_storage.oauth2_consumers
+ end
+
+ oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ do
+ Result := oauth_20_storage.oauth_consumer_by_name (a_name)
+ end
+
+ oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by callback `a_callback', if any.
+ do
+ Result := oauth_20_storage.oauth_consumer_by_callback (a_callback)
+ end
+
+feature -- Change: User OAuth20
+
+
+ new_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_GENERAL)
+ -- Add a new user with oauth20 using the consumer `a_consumer'.
+ require
+ has_id: a_user.has_id
+ do
+ oauth_20_storage.new_user_oauth2 (a_token, a_user_profile, a_user, a_consumer)
+ end
+
+
+ update_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Update user `a_user' with oauth2 for the consumer `a_consumer'.
+ require
+ has_id: a_user.has_id
+ do
+ oauth_20_storage.update_user_oauth2 (a_token, a_user_profile, a_user, a_consumer_table)
+ end
+
+
+ remove_user_oauth2 (a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Remove user `a_user' with oauth2 for the consumer `a_consumer'.
+ require
+ has_id: a_user.has_id
+ do
+ oauth_20_storage.remove_user_oauth2 (a_user, a_consumer_table)
+ end
+
+
+
+end
diff --git a/modules/oauth20/cms_oauth_20_constants.e b/modules/oauth20/cms_oauth_20_constants.e
new file mode 100644
index 0000000..eebe6f1
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_constants.e
@@ -0,0 +1,21 @@
+note
+ description: "Summary description for {CMS_OAUTH_20_CONSTANTS}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_CONSTANTS
+
+feature -- Access
+
+ oauth_session: STRING = "EWF_ROC_OAUTH_TOKEN_"
+ -- Name of Cookie used to keep the session info.
+ -- FIXME: make this configurable.
+
+ oauth_callback: STRING = "callback"
+ -- Callback parameter.
+
+ oauth_code: STRING = "code"
+ -- Code query parameter.
+
+end
diff --git a/modules/oauth20/cms_oauth_20_consumer.e b/modules/oauth20/cms_oauth_20_consumer.e
new file mode 100644
index 0000000..1e14806
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_consumer.e
@@ -0,0 +1,156 @@
+note
+ description: "Summary description for {CMS_OAUTH_CONSUMER}."
+ author: ""
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_CONSUMER
+
+inherit
+ ANY
+ redefine
+ default_create
+ end
+
+create
+ default_create,
+ make_with_id
+
+feature {NONE} -- Initialization
+
+ make_with_id (a_id: like id)
+ do
+ id := a_id
+ default_create
+ end
+
+ default_create
+ do
+ set_endpoint ("")
+ set_authorize_url ("")
+ set_extractor ("")
+ set_callback_name ("")
+ set_protected_resource_url ("")
+ set_scope ("")
+ set_api_key ("")
+ set_api_secret ("")
+ set_name ("")
+ end
+
+feature -- Access
+
+ endpoint: READABLE_STRING_8
+ -- Url that receives the access token request.
+
+ authorize_url: READABLE_STRING_8
+ --
+
+ extractor: READABLE_STRING_8
+ -- text, json
+
+
+ callback_name: READABLE_STRING_8
+ -- consumer callback name
+
+ protected_resource_url: READABLE_STRING_8
+ -- consumer resource url
+
+ scope: READABLE_STRING_8
+ -- consumer scope
+
+ api_key: READABLE_STRING_8
+ -- consumer public key
+
+ api_secret: READABLE_STRING_8
+ -- consumer secret.
+
+ name: READABLE_STRING_32
+ -- consumer name.
+
+ id: INTEGER_64
+ -- unique identifier.
+
+feature -- Element change
+
+ set_extractor (a_extractor: like extractor)
+ -- Assign `extractor' with `a_extractor'.
+ do
+ extractor := a_extractor
+ ensure
+ extractor_assigned: extractor = a_extractor
+ end
+
+ set_authorize_url (a_authorize_url: like authorize_url)
+ -- Assign `authorize_url' with `a_authorize_url'.
+ do
+ authorize_url := a_authorize_url
+ ensure
+ authorize_url_assigned: authorize_url = a_authorize_url
+ end
+
+ set_endpoint (a_endpoint: like endpoint)
+ -- Assign `endpoint' with `a_endpoint'.
+ do
+ endpoint := a_endpoint
+ ensure
+ endpoint_assigned: endpoint = a_endpoint
+ end
+
+ set_callback_name (a_callback_name: like callback_name)
+ -- Assign `callback_name' with `a_callback_name'.
+ do
+ callback_name := a_callback_name
+ ensure
+ callback_name_assigned: callback_name = a_callback_name
+ end
+
+ set_protected_resource_url (a_protected_resource_url: like protected_resource_url)
+ -- Assign `protected_resource_url' with `a_protected_resource_url'.
+ do
+ protected_resource_url := a_protected_resource_url
+ ensure
+ protected_resource_url_assigned: protected_resource_url = a_protected_resource_url
+ end
+
+ set_scope (a_scope: like scope)
+ -- Assign `scope' with `a_scope'.
+ do
+ scope := a_scope
+ ensure
+ scope_assigned: scope = a_scope
+ end
+
+ set_api_key (an_api_key: like api_key)
+ -- Assign `api_key' with `an_api_key'.
+ do
+ api_key := an_api_key
+ ensure
+ api_key_assigned: api_key = an_api_key
+ end
+
+ set_api_secret (an_api_secret: like api_secret)
+ -- Assign `api_secret' with `an_api_secret'.
+ do
+ api_secret := an_api_secret
+ ensure
+ api_secret_assigned: api_secret = an_api_secret
+ end
+
+ set_name (a_name: like name)
+ -- Assign `name' with `a_name'.
+ do
+ name := a_name
+ ensure
+ name_assigned: name = a_name
+ end
+
+ set_id (an_id: like id)
+ -- Assign `id' with `an_id'.
+ do
+ id := an_id
+ ensure
+ id_assigned: id = an_id
+ end
+
+end
diff --git a/modules/oauth20/cms_oauth_20_generic_api.e b/modules/oauth20/cms_oauth_20_generic_api.e
new file mode 100644
index 0000000..552d2ea
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_generic_api.e
@@ -0,0 +1,94 @@
+note
+ description: "Generic OAUTH2 API"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_GENERIC_API
+
+inherit
+
+ OAUTH_20_API
+ redefine
+ access_token_extractor,
+ access_token_verb
+ end
+
+create
+ make
+
+feature {NONE} -- Initialize
+
+ make (a_endpoint: READABLE_STRING_8; a_authorize_url: READABLE_STRING_8; a_extractor: READABLE_STRING_8)
+ do
+ endpoint := a_endpoint
+ authorize_url := a_authorize_url
+ extractor := a_extractor
+ ensure
+ endpoint_set: endpoint = a_endpoint
+ authorize_url_set: authorize_url = a_authorize_url
+ extractor_set: extractor = a_authorize_url
+ end
+
+ endpoint: READABLE_STRING_8
+ -- Url that receives the access token request.
+
+ authorize_url: READABLE_STRING_8
+ --
+
+ extractor: READABLE_STRING_8
+ -- text, json
+
+feature -- Access
+
+ access_token_extractor: ACCESS_TOKEN_EXTRACTOR
+ -- Return token extractor, by default TOKEN_EXTRACTOR_20.
+ do
+ if extractor.is_case_insensitive_equal_general ("json") then
+ create {JSON_TOKEN_EXTRACTOR} Result
+ else
+ create {TOKEN_EXTRACTOR_20} Result
+ end
+ end
+
+ access_token_verb: STRING_8
+ do
+ Result := "POST"
+ end
+
+ access_token_endpoint: STRING_8
+ -- Url that receives the access token request
+ do
+ create Result.make_from_string (endpoint)
+ end
+
+ authorization_url (config: OAUTH_CONFIG): detachable STRING_8
+ -- Url where you should redirect your users to authneticate
+ local
+ l_result: STRING_8
+ do
+ if attached config.scope as l_scope then
+ create l_result.make_from_string (authorize_url + SCOPED_AUTHORIZE_URL)
+ l_result.replace_substring_all ("$CLIENT_ID", config.api_key.as_string_8)
+ if attached config.callback as l_callback then
+ l_result.replace_substring_all ("$REDIRECT_URI", (create {OAUTH_ENCODER}).encoded_string (l_callback.as_string_8))
+ end
+ if attached config.callback as l_callback then
+ l_result.replace_substring_all ("$SCOPE", (create {OAUTH_ENCODER}).encoded_string (l_scope.as_STRING_8))
+ Result := l_result
+ end
+ else
+ create l_result.make_from_string (authorize_url + SCOPED_AUTHORIZE_URL)
+ l_result.replace_substring_all ("$CLIENT_ID", config.api_key.as_string_8)
+ if attached config.callback as l_callback then
+ l_result.replace_substring_all ("$REDIRECT_URI", (create {OAUTH_ENCODER}).encoded_string (l_callback.as_string_8))
+ end
+ end
+ end
+
+feature -- Implementation
+
+ Scoped_authorize_url: STRING = "&scope=$SCOPE";
+
+
+end
diff --git a/modules/oauth20/cms_oauth_20_module.e b/modules/oauth20/cms_oauth_20_module.e
new file mode 100644
index 0000000..7cf6bab
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_module.e
@@ -0,0 +1,611 @@
+note
+ description: "Generic OAuth Module supporting authentication using different providers."
+ date: "$Date: 2015-05-20 06:50:50 -0300 (mi. 20 de may. de 2015) $"
+ revision: "$Revision: 97328 $"
+
+class
+ CMS_OAUTH_20_MODULE
+
+inherit
+ CMS_MODULE
+ rename
+ module_api as user_oauth_api
+ redefine
+ filters,
+ setup_hooks,
+ initialize,
+ install,
+ user_oauth_api
+ end
+
+
+ CMS_HOOK_BLOCK
+
+ CMS_HOOK_AUTO_REGISTER
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_VALUE_TABLE_ALTER
+
+ SHARED_EXECUTION_ENVIRONMENT
+ export
+ {NONE} all
+ end
+
+ REFACTORING_HELPER
+
+ SHARED_LOGGER
+
+ CMS_REQUEST_UTIL
+
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create current module
+ do
+ version := "1.0"
+ description := "OAuth20 module"
+ package := "authentication"
+
+ add_dependency ({CMS_AUTHENTICATION_MODULE})
+
+ create root_dir.make_current
+ cache_duration := 0
+ end
+
+feature -- Access
+
+ name: STRING = "oauth20"
+
+feature {CMS_API} -- Module Initialization
+
+ initialize (a_api: CMS_API)
+ --
+ local
+ l_user_auth_api: like user_oauth_api
+ l_user_auth_storage: CMS_OAUTH_20_STORAGE_I
+ do
+ Precursor (a_api)
+
+ -- Storage initialization
+ if attached a_api.storage.as_sql_storage as l_storage_sql then
+ create {CMS_OAUTH_20_STORAGE_SQL} l_user_auth_storage.make (l_storage_sql)
+ else
+ -- FIXME: in case of NULL storage, should Current be disabled?
+ create {CMS_OAUTH_20_STORAGE_NULL} l_user_auth_storage
+ end
+
+ -- API initialization
+ create l_user_auth_api.make_with_storage (a_api, l_user_auth_storage)
+ user_oauth_api := l_user_auth_api
+ ensure then
+ user_oauth_api_set: user_oauth_api /= Void
+ end
+
+feature {CMS_API} -- Module management
+
+ install (api: CMS_API)
+ local
+ l_consumers: LIST [STRING]
+ do
+ -- Schema
+ if attached api.storage.as_sql_storage as l_sql_storage then
+ if not l_sql_storage.sql_table_exists ("oauth2_consumers") then
+ --| Schema
+ l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers.sql")), Void)
+
+ if l_sql_storage.has_error then
+ api.logger.put_error ("Could not initialize database for oauth_20 module", generating_type)
+ end
+ -- TODO workaround.
+ l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers_initialize.sql")), Void)
+ end
+
+ -- TODO workaround, until we have an admin module
+ l_sql_storage.sql_query ("SELECT name FROM oauth2_consumers;", Void)
+ if l_sql_storage.has_error then
+ api.logger.put_error ("Could not initialize database for differnent consumers", generating_type)
+ else
+ from
+ l_sql_storage.sql_start
+ create {ARRAYED_LIST [STRING]} l_consumers.make (2)
+ until
+ l_sql_storage.sql_after
+ loop
+ if attached l_sql_storage.sql_read_string (1) as l_name then
+ l_consumers.force ("oauth2_" + l_name)
+ end
+ l_sql_storage.sql_forth
+ end
+ l_sql_storage.sql_finalize
+ across l_consumers as ic loop
+ if not l_sql_storage.sql_table_exists (ic.item) then
+ if attached l_sql_storage.sql_script_content (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_table.sql.tpl"))) as sql then
+ -- FIXME: shouldn't we use a unique table for all oauth providers? or as it is .. one table per oauth provider?
+ sql.replace_substring_all ("$table_name", ic.item)
+ l_sql_storage.sql_execute_script (sql, Void)
+ end
+ end
+ end
+ end
+ l_sql_storage.sql_finalize
+ Precursor {CMS_MODULE}(api)
+ end
+ end
+
+feature {CMS_API} -- Access: API
+
+ user_oauth_api: detachable CMS_OAUTH_20_API
+ --
+
+feature -- Filters
+
+ filters (a_api: CMS_API): detachable LIST [WSF_FILTER]
+ -- Possibly list of Filter's module.
+ do
+ create {ARRAYED_LIST [WSF_FILTER]} Result.make (1)
+ if attached user_oauth_api as l_user_oauth_api then
+ Result.extend (create {CMS_OAUTH_20_FILTER}.make (a_api, l_user_oauth_api))
+ end
+ end
+
+feature -- Access: docs
+
+ root_dir: PATH
+
+ cache_duration: INTEGER
+ -- Caching duration
+ --| 0: disable
+ --| -1: cache always valie
+ --| nb: cache expires after nb seconds.
+
+ cache_disabled: BOOLEAN
+ do
+ Result := cache_duration = 0
+ end
+
+feature -- Router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ if attached user_oauth_api as l_user_oauth_api then
+ configure_web (a_api, l_user_oauth_api, a_router)
+ end
+ end
+
+ configure_web (a_api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API; a_router: WSF_ROUTER)
+ do
+ a_router.handle ("/account/roc-oauth-login", create {WSF_URI_AGENT_HANDLER}.make (agent handle_login (a_api, ?, ?)), a_router.methods_head_get)
+ a_router.handle ("/account/roc-oauth-logout", create {WSF_URI_AGENT_HANDLER}.make (agent handle_logout (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/login-with-oauth/{callback}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_login_with_oauth (a_api,a_user_oauth_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/oauth-callback/{callback}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_callback_oauth (a_api, a_user_oauth_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/oauth-associate", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_associate (a_api, a_user_oauth_api, ?, ?)), a_router.methods_post)
+ a_router.handle ("/account/oauth-un-associate", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_un_associate (a_api, a_user_oauth_api, ?, ?)), a_router.methods_post)
+ end
+
+feature -- Hooks configuration
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ -- Module hooks configuration.
+ do
+ auto_subscribe_to_hooks (a_hooks)
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_value_table_alter_hook (Current)
+ end
+
+feature -- Hooks
+
+ value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE)
+ --
+ do
+ if
+ attached a_response.user as u and then
+ attached {WSF_STRING} a_response.request.cookie ({CMS_OAUTH_20_CONSTANTS}.oauth_session)
+ then
+ a_value.force ("account/roc-oauth-logout", "auth_login_strategy")
+ end
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ -- Hook execution on collection of menu contained by `a_menu_system'
+ -- for related response `a_response'.
+ local
+ lnk: CMS_LOCAL_LINK
+ lnk2: detachable CMS_LINK
+ do
+ if
+ attached a_response.user as u and then
+ attached {WSF_STRING} a_response.request.cookie ({CMS_OAUTH_20_CONSTANTS}.oauth_session) as l_roc_auth_session_token
+ then
+ across
+ a_menu_system.primary_menu.items as ic
+ until
+ lnk2 /= Void
+ loop
+ if
+ ic.item.location.same_string ("account/roc-logout") or else
+ ic.item.location.same_string ("basic_auth_logoff")
+ then
+ lnk2 := ic.item
+ end
+ end
+ if lnk2 /= Void then
+ a_menu_system.primary_menu.remove (lnk2)
+ end
+ create lnk.make ("Logout", "account/roc-oauth-logout" )
+ a_menu_system.primary_menu.extend (lnk)
+ else
+ if a_response.location.starts_with ("account/") then
+ create lnk.make ("OAuth", "account/roc-oauth-login")
+ a_response.add_to_primary_tabs (lnk)
+ end
+ end
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ local
+ l_string: STRING
+ do
+ Result := <<"login", "account">>
+ debug ("roc")
+ create l_string.make_empty
+ across
+ Result as ic
+ loop
+ l_string.append (ic.item)
+ l_string.append_character (' ')
+ end
+ write_debug_log (generator + ".block_list:" + l_string )
+ end
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if
+ a_block_id.is_case_insensitive_equal_general ("login") and then
+ a_response.location.starts_with ("account/roc-oauth-login")
+ then
+ get_block_view_login (a_block_id, a_response)
+ elseif a_block_id.is_case_insensitive_equal_general ("account") and then
+ a_response.location.same_string ("account")
+ then
+ if
+ attached template_block ("account_info", a_response) as l_tpl_block and then
+ attached a_response.user as l_user
+ then
+ associate_account (l_user, a_response.values)
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [resources_page]")
+ end
+ end
+ end
+ end
+
+ handle_login (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_value ("Login", "optional_content_type")
+ r.execute
+ end
+
+ handle_logout (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_cookie: WSF_COOKIE
+ do
+ if
+ attached {WSF_STRING} req.cookie ({CMS_OAUTH_20_CONSTANTS}.oauth_session) as l_cookie_token and then
+ attached {CMS_USER} current_user (req) as l_user
+ then
+ -- Logout OAuth
+ create l_cookie.make ({CMS_OAUTH_20_CONSTANTS}.oauth_session, l_cookie_token.value)
+ l_cookie.set_path ("/")
+ l_cookie.set_max_age (-1)
+ res.add_cookie (l_cookie)
+ unset_current_user (req)
+
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_status_code ({HTTP_CONSTANTS}.found)
+ r.set_redirection (req.absolute_script_url (""))
+ r.execute
+ else
+ fixme (generator + ": missing else implementation in handle_logout!")
+ end
+ end
+
+feature {NONE} -- Associate
+
+ associate_account (a_user: CMS_USER; a_value: CMS_VALUE_TABLE)
+ local
+ l_associated: LIST [STRING]
+ l_not_associated: LIST [STRING]
+ do
+ if attached user_oauth_api as l_oauth_api then
+ create {ARRAYED_LIST [STRING]} l_associated.make (1)
+ create {ARRAYED_LIST [STRING]} l_not_associated.make (1)
+ across l_oauth_api.oauth2_consumers as ic loop
+ if attached l_oauth_api.user_oauth2_by_id (a_user.id, ic.item) then
+ l_associated.force (ic.item)
+ else
+ l_not_associated.force (ic.item)
+ end
+ end
+ a_value.force (l_associated, "oauth_associated")
+ a_value.force (l_not_associated, "oauth_not_associated")
+ end
+ end
+
+feature {NONE} -- Helpers
+
+ template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK
+ -- Smarty content block for `a_block_id'
+ local
+ p: detachable PATH
+ do
+ create p.make_from_string ("templates")
+ p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl")
+ p := a_response.api.module_theme_resource_location (Current, p)
+ if p /= Void then
+ if attached p.entry as e then
+ create Result.make (a_block_id, Void, p.parent, e)
+ else
+ create Result.make (a_block_id, Void, p.parent, p)
+ end
+ end
+ end
+
+feature {NONE} -- Block views
+
+ get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ local
+ vals: CMS_VALUE_TABLE
+ do
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ create vals.make (1)
+ -- add the variable to the block
+ value_table_alter (vals, a_response)
+ across
+ vals as ic
+ loop
+ l_tpl_block.set_value (ic.item, ic.key)
+ end
+ if
+ attached user_oauth_api as l_auth_api and then
+ attached l_auth_api.oauth2_consumers as l_list
+ then
+ l_tpl_block.set_value (l_list, "oauth_consumers")
+ end
+
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+
+
+feature -- OAuth2 Login with Provider
+
+ handle_login_with_oauth (api: CMS_API; a_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_oauth: CMS_OAUTH_20_WORKFLOW
+ do
+ if
+ attached {WSF_STRING} req.path_parameter ({CMS_OAUTH_20_CONSTANTS}.oauth_callback) as p_consumer and then
+ attached {CMS_OAUTH_20_CONSUMER} a_oauth_api.oauth_consumer_by_name (p_consumer.value) as l_consumer
+ then
+ create l_oauth.make (req.server_url, l_consumer)
+ if attached l_oauth.authorization_url as l_authorization_url then
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_redirection (l_authorization_url)
+ r.execute
+ else
+ create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("Bad request")
+ r.execute
+ end
+ else
+ create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("Bad request")
+ r.execute
+ end
+ end
+
+ handle_callback_oauth (api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_auth: CMS_OAUTH_20_WORKFLOW
+ l_user_api: CMS_USER_API
+ l_user: CMS_USER
+ l_roles: LIST [CMS_USER_ROLE]
+ l_cookie: WSF_COOKIE
+ es: CMS_AUTHENTICATON_EMAIL_SERVICE
+ do
+ if attached {WSF_STRING} req.path_parameter ({CMS_OAUTH_20_CONSTANTS}.oauth_callback) as l_callback and then
+ attached {CMS_OAUTH_20_CONSUMER} a_user_oauth_api.oauth_consumer_by_callback (l_callback.value) as l_consumer and then
+ attached {WSF_STRING} req.query_parameter ({CMS_OAUTH_20_CONSTANTS}.oauth_code) as l_code
+ then
+ create l_auth.make (req.server_url, l_consumer)
+ l_auth.sign_request (l_code.value)
+ if
+ attached l_auth.access_token as l_access_token and then
+ attached l_auth.user_profile as l_user_profile
+ then
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ -- extract user email
+ -- check if the user exist
+ l_user_api := api.user_api
+ -- 1 if the user exit put it in the context
+ if
+ attached l_auth.user_email as l_email
+ then
+ if attached l_user_api.user_by_email (l_email) as p_user then
+ -- User with email exist
+ if attached a_user_oauth_api.user_oauth2_by_id (p_user.id, l_consumer.name) then
+ -- Update oauth entry
+ a_user_oauth_api.update_user_oauth2 (l_access_token.token, l_user_profile, p_user, l_consumer.name )
+ else
+ -- create a oauth entry
+ a_user_oauth_api.new_user_oauth2 (l_access_token.token, l_user_profile, p_user, l_consumer.name )
+ end
+ create l_cookie.make ({CMS_OAUTH_20_CONSTANTS}.oauth_session, l_access_token.token)
+ l_cookie.set_max_age (l_access_token.expires_in)
+ l_cookie.set_path ("/")
+ res.add_cookie (l_cookie)
+ elseif attached a_user_oauth_api.user_oauth2_by_email (l_email, l_consumer.name) as p_user then
+ a_user_oauth_api.update_user_oauth2 (l_access_token.token, l_user_profile, p_user, l_consumer.name )
+ create l_cookie.make ({CMS_OAUTH_20_CONSTANTS}.oauth_session, l_access_token.token)
+ l_cookie.set_max_age (l_access_token.expires_in)
+ l_cookie.set_path ("/")
+ res.add_cookie (l_cookie)
+ else
+ create {ARRAYED_LIST [CMS_USER_ROLE]} l_roles.make (1)
+ l_roles.force (l_user_api.authenticated_user_role)
+
+ -- Create a new user and oauth entry
+ create l_user.make (l_email)
+ l_user.set_email (l_email)
+ l_user.set_password (new_token) -- generate a random password.
+ l_user.set_roles (l_roles)
+ l_user.mark_active
+ l_user_api.new_user (l_user)
+
+ -- Add oauth entry
+ a_user_oauth_api.new_user_oauth2 (l_access_token.token, l_user_profile, l_user, l_consumer.name )
+ create l_cookie.make ({CMS_OAUTH_20_CONSTANTS}.oauth_session, l_access_token.token)
+ l_cookie.set_max_age (l_access_token.expires_in)
+ l_cookie.set_path ("/")
+ res.add_cookie (l_cookie)
+ set_current_user (req, l_user)
+
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle_callback_oauth: send_contact_welcome_email")
+ es.send_contact_welcome_email (l_email, "")
+ end
+ end
+ r.set_redirection (r.front_page_url)
+ r.execute
+ end
+
+ end
+
+ end
+
+ handle_associate (api: CMS_API; a_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+
+ if req.is_post_request_method then
+ if
+ attached {WSF_STRING} req.form_parameter ("consumer") as l_consumer and then
+ attached {WSF_STRING} req.form_parameter ("email") as l_email and then
+ attached r.user as l_user
+ then
+ l_user.set_email (l_email.value)
+ a_oauth_api.new_user_oauth2 ("none", "none", l_user, l_consumer.value )
+ -- TODO send email?
+ end
+ end
+ r.set_redirection (req.absolute_script_url ("/account"))
+ r.execute
+ end
+
+
+ handle_un_associate (api: CMS_API; a_oauth_api: CMS_OAUTH_20_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if req.is_post_request_method then
+ if
+ attached {WSF_STRING} req.form_parameter ("consumer") as l_consumer and then
+ attached r.user as l_user
+ then
+ a_oauth_api.remove_user_oauth2 (l_user, l_consumer.value)
+ -- TODO send email?
+ end
+ end
+ r.set_redirection (req.absolute_script_url ("/account"))
+ r.execute
+ end
+
+
+
+feature {NONE} -- Token Generation
+
+ new_token: STRING
+ -- Generate a new token activation token
+ local
+ l_token: STRING
+ l_security: SECURITY_PROVIDER
+ l_encode: URL_ENCODER
+ do
+ create l_security
+ l_token := l_security.token
+ create l_encode
+ from until l_token.same_string (l_encode.encoded_string (l_token)) loop
+ -- Loop ensure that we have a security token that does not contain characters that need encoding.
+ -- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token
+ -- but the user will need to use an unencoded token if activation has to be done manually.
+ l_token := l_security.token
+ end
+ Result := l_token
+ end
+
+feature {NONE} -- Implementation: date and time
+
+ http_date_format_to_date (s: READABLE_STRING_8): detachable DATE_TIME
+ local
+ d: HTTP_DATE
+ do
+ create d.make_from_string (s)
+ if not d.has_error then
+ Result := d.date_time
+ end
+ end
+
+ file_date (p: PATH): DATE_TIME
+ require
+ path_exists: (create {FILE_UTILITIES}).file_path_exists (p)
+ local
+ f: RAW_FILE
+ do
+ create f.make_with_path (p)
+ Result := timestamp_to_date (f.date)
+ end
+
+ timestamp_to_date (n: INTEGER): DATE_TIME
+ local
+ d: HTTP_DATE
+ do
+ create d.make_from_timestamp (n)
+ Result := d.date_time
+ end
+
+
+note
+ copyright: "Copyright (c) 1984-2013, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/modules/oauth20/cms_oauth_20_workflow.e b/modules/oauth20/cms_oauth_20_workflow.e
new file mode 100644
index 0000000..039020c
--- /dev/null
+++ b/modules/oauth20/cms_oauth_20_workflow.e
@@ -0,0 +1,133 @@
+note
+ description: "OAuth workflow"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_WORKFLOW
+
+inherit
+
+ SHARED_LOGGER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_host: READABLE_STRING_32; a_consumer: CMS_OAUTH_20_CONSUMER)
+ -- Create an object with the host `a_host'.
+ do
+ initilize (a_consumer)
+ create config.make_default (a_consumer.api_key, a_consumer.api_secret)
+ config.set_callback (a_host + "/account/oauth-callback/"+ a_consumer.callback_name)
+ config.set_scope (a_consumer.scope)
+ --Todo create a generic OAUTH_20_GENERIC_API
+ create oauth_api.make (a_consumer.endpoint, a_consumer.authorize_url, a_consumer.extractor)
+ api_service := oauth_api.create_service (config)
+ end
+
+ initilize (a_consumer: CMS_OAUTH_20_CONSUMER)
+ do
+ --Use configuration values if any if not defaul
+ api_key := a_consumer.api_key
+ api_secret := a_consumer.api_secret
+ scope := a_consumer.scope
+ protected_resource_url := a_consumer.protected_resource_url
+ end
+
+feature -- Access
+
+ authorization_url: detachable READABLE_STRING_32
+ -- Obtain the Authorization URL.
+ do
+ -- Obtain the Authorization URL
+ write_debug_log (generator + ".authorization_url Fetching the Authorization URL..!")
+ if attached api_service.authorization_url (empty_token) as l_authorization_url then
+ write_debug_log (generator + ".authorization_url: Got the Authorization URL!")
+ write_debug_log (generator + ".authorization_url:" + l_authorization_url)
+ Result := l_authorization_url.as_string_32
+ end
+ end
+
+ sign_request (a_code: READABLE_STRING_32)
+ -- Sign request with code `a_code'.
+ --! To get the code `a_code' you need to do a request
+ --! using the authorization_url
+ local
+ request: OAUTH_REQUEST
+ do
+ -- Get the access token.
+ write_debug_log (generator + ".sign_request Fetching the access token with code [" + a_code + "]")
+ access_token := api_service.access_token_post (empty_token, create {OAUTH_VERIFIER}.make (a_code))
+ if attached access_token as l_access_token then
+ write_debug_log (generator + ".sign_request Got the Access Token [" + l_access_token.debug_output + "]")
+ -- Get the user email
+ --! at the moment the scope is mail, but we can change it to get more information.
+ create request.make ("GET", protected_resource_url)
+ request.add_header ("Authorization", "Bearer " + l_access_token.token)
+ api_service.sign_request (l_access_token, request)
+ if attached {OAUTH_RESPONSE} request.execute as l_response then
+ write_debug_log (generator + ".sign_request Sign_request response [" + l_response.status.out + "]")
+ if attached l_response.body as l_body then
+ user_profile := l_body
+ write_debug_log (generator + ".sign_request User profile [" + l_body + "]")
+ end
+ end
+ end
+ end
+
+ user_email: detachable READABLE_STRING_32
+ -- Retrieve user email if any.
+ local
+ l_json: JSON_CONFIG
+ do
+ if attached user_profile as l_profile then
+ create l_json.make_from_string (l_profile)
+ if
+ attached {JSON_ARRAY} l_json.item ("emails") as l_array and then
+ attached {JSON_OBJECT} l_array.i_th (1) as l_object and then
+ attached {JSON_STRING} l_object.item ("value") as l_email
+ then
+ Result := l_email.item
+ elseif attached {JSON_STRING} l_json.item ("email") as l_email then
+ Result := l_email.unescaped_string_32
+ end
+ end
+ end
+
+feature -- Access
+
+ access_token: detachable OAUTH_TOKEN
+ -- JSON representing the access token.
+
+ user_profile: detachable READABLE_STRING_32
+ -- JSON representing the user profiles.
+
+feature {NONE} -- Implementation
+
+ oauth_api: CMS_OAUTH_20_GENERIC_API
+ -- OAuth 2.0 Google API.
+
+ config: OAUTH_CONFIG
+ -- configuration.
+
+ api_service: OAUTH_SERVICE_I
+ -- Service.
+
+ api_key: STRING
+ -- public key.
+
+ api_secret: STRING
+ -- secret key.
+
+ scope: STRING
+ -- api scope to access protected resources.
+
+ protected_resource_url: STRING
+ -- Resource url.
+
+ empty_token: detachable OAUTH_TOKEN
+ -- fake token.
+
+end
diff --git a/modules/oauth20/filter/cms_oauth_20_filter.e b/modules/oauth20/filter/cms_oauth_20_filter.e
new file mode 100644
index 0000000..ead00a1
--- /dev/null
+++ b/modules/oauth20/filter/cms_oauth_20_filter.e
@@ -0,0 +1,54 @@
+note
+ description: "[
+ Extracts an OAuth2 token from the incoming request (cookie) and uses it to populate the user (or cms user context)
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_FILTER
+
+inherit
+ WSF_URI_TEMPLATE_HANDLER
+ CMS_HANDLER
+ rename
+ make as make_handler
+ end
+
+ WSF_FILTER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_api: CMS_API; a_user_oauth_api: CMS_OAUTH_20_API)
+ do
+ make_handler (a_api)
+ user_oauth_api := a_user_oauth_api
+ end
+
+ user_oauth_api: CMS_OAUTH_20_API
+
+feature -- Basic operations
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute the filter.
+ do
+ api.logger.put_debug (generator + ".execute ", Void)
+ -- A valid user
+ if
+ attached {WSF_STRING} req.cookie ({CMS_OAUTH_20_CONSTANTS}.oauth_session) as l_roc_auth_session_token
+ then
+ if attached user_oauth_api.user_oauth2_without_consumer_by_token (l_roc_auth_session_token.value) as l_user then
+ set_current_user (req, l_user)
+ else
+ api.logger.put_error (generator + ".execute login_valid failed for: " + l_roc_auth_session_token.value , Void)
+ end
+ else
+ api.logger.put_debug (generator + ".execute without authentication", Void)
+ end
+ execute_next (req, res)
+ end
+
+end
diff --git a/modules/oauth20/oauth20-safe.ecf b/modules/oauth20/oauth20-safe.ecf
new file mode 100644
index 0000000..2fdfa3a
--- /dev/null
+++ b/modules/oauth20/oauth20-safe.ecf
@@ -0,0 +1,29 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/oauth20/persistence/cms_oauth_20_storage_i.e b/modules/oauth20/persistence/cms_oauth_20_storage_i.e
new file mode 100644
index 0000000..d9fcf5e
--- /dev/null
+++ b/modules/oauth20/persistence/cms_oauth_20_storage_i.e
@@ -0,0 +1,78 @@
+note
+ description: "[
+ API to handle OAUTH storage
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_OAUTH_20_STORAGE_I
+
+inherit
+ SHARED_LOGGER
+
+feature -- Error Handling
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ deferred
+ end
+
+feature -- Access: Users
+
+ user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by id `a_uid' for the consumer `a_consumer', if aby.
+ deferred
+ end
+
+ user_oauth2_by_email (a_email: like {CMS_USER}.email; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by email `a_email' for the consumer `a_consumer', if any.
+ deferred
+ end
+
+ user_oauth2_by_token (a_token: READABLE_STRING_GENERAL; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by token `a_token' for the consumer `a_consumer'.
+ deferred
+ end
+
+ user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve user by token `a_token' searching in all the registered consumers in the system.
+ deferred
+ end
+
+feature -- Access: Consumers
+
+ oauth2_consumers: LIST [STRING]
+ deferred
+ end
+
+ oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ deferred
+ end
+
+ oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by callback `a_callback', if any.
+ deferred
+ end
+
+feature -- Change: User Oauth2
+
+ new_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Add a new user with oauth2 authentication.
+ deferred
+ end
+
+ update_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL )
+ -- Update user `a_user' with oauth2 authentication.
+ deferred
+ end
+
+ remove_user_oauth2 (a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Remove user `a_user' with oauth2 for the consumer `a_consumer'.
+ require
+ has_id: a_user.has_id
+ deferred
+ end
+
+end
diff --git a/modules/oauth20/persistence/cms_oauth_20_storage_null.e b/modules/oauth20/persistence/cms_oauth_20_storage_null.e
new file mode 100644
index 0000000..8ddef02
--- /dev/null
+++ b/modules/oauth20/persistence/cms_oauth_20_storage_null.e
@@ -0,0 +1,80 @@
+note
+ description: "Summary description for {CMS_OAUTH_20_STORAGE_NULL}."
+ author: ""
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_STORAGE_NULL
+
+inherit
+
+ CMS_OAUTH_20_STORAGE_I
+
+
+feature -- Error handler
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ do
+ create Result.make
+ end
+
+feature -- Access: Users
+
+ user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- CMS User with Oauth credential by id if any.
+ do
+ end
+
+ user_oauth2_by_email (a_email: like {CMS_USER}.email; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ do
+ end
+
+ user_oauth2_by_token (a_token: READABLE_STRING_GENERAL; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- -- CMS User with Oauth credential by access token `a_token' if any.
+ do
+ end
+
+ user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_GENERAL ): detachable CMS_USER
+ do
+ end
+
+feature -- Access: Consumers
+
+ oauth2_consumers: LIST [STRING]
+ do
+ create {ARRAYED_LIST [STRING]} Result.make (0)
+ end
+
+ oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ do
+ end
+
+ oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by callback `a_callback', if any.
+ do
+ end
+
+feature -- Change: User Oauth2
+
+ new_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Add a new user with oauth2 authentication.
+ do
+ end
+
+ update_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL )
+ -- Update user `a_user' with oauth2 authentication.
+ do
+ end
+
+ remove_user_oauth2 (a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL)
+ -- Remove user `a_user' with oauth2 for the consumer `a_consumer'.
+ do
+ end
+
+
+
+end
diff --git a/modules/oauth20/persistence/cms_oauth_20_storage_sql.e b/modules/oauth20/persistence/cms_oauth_20_storage_sql.e
new file mode 100644
index 0000000..6b6f0b5
--- /dev/null
+++ b/modules/oauth20/persistence/cms_oauth_20_storage_sql.e
@@ -0,0 +1,379 @@
+note
+ description: "Summary description for {CMS_OAUTH_20_STORAGE_SQL}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OAUTH_20_STORAGE_SQL
+
+inherit
+ CMS_OAUTH_20_STORAGE_I
+
+ CMS_PROXY_STORAGE_SQL
+
+ CMS_OAUTH_20_STORAGE_I
+
+ CMS_STORAGE_SQL_I
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Access User Outh
+
+ user_oauth2_without_consumer_by_token (a_token: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve user by token `a_token' searching in all the registered consumers in the system.
+ local
+ l_list: LIST [STRING]
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_oauth2_without_consumer_by_token")
+ l_list := oauth2_consumers
+ from
+ l_list.start
+ until
+ l_list.after or Result /= Void
+ loop
+ Result := user_oauth2_by_token (a_token, l_list.item)
+ l_list.forth
+ end
+ end
+
+ user_oauth2_by_id (a_uid: like {CMS_USER}.id; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_oauth2_by_id")
+ create l_parameters.make (1)
+ l_parameters.put (a_uid, "uid")
+ create l_string.make_from_string (select_user_oauth2_template_by_id)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_query (l_string, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_user
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+ user_oauth2_by_email (a_email: like {CMS_USER}.email; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_oauth2_by_email")
+ create l_parameters.make (1)
+ l_parameters.put (a_email, "email")
+ create l_string.make_from_string (select_user_oauth2_template_by_email)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_query (l_string, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_user
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+ user_oauth2_by_token (a_token: READABLE_STRING_GENERAL; a_consumer: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_by_oauth2_token")
+ create l_parameters.make (1)
+ l_parameters.put (a_token, "token")
+ create l_string.make_from_string (select_user_by_oauth2_template_token)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_query (l_string, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_user
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+
+feature --Access: Consumers
+
+ oauth2_consumers: LIST [STRING]
+ -- Return a list of consumers, or empty
+ do
+ error_handler.reset
+ create {ARRAYED_LIST [STRING]} Result.make (0)
+ write_information_log (generator + ".user_by_oauth2_token")
+ sql_query (Sql_oauth_consumers, Void)
+ if not has_error then
+ from
+ sql_start
+ until
+ sql_after
+ loop
+ if attached sql_read_string (1) as l_name then
+ Result.force (l_name)
+ end
+ sql_forth
+ end
+ end
+ sql_finalize
+ end
+
+ oauth_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".oauth_consumer_by_name")
+ create l_parameters.make (1)
+ l_parameters.put (a_name, "name")
+ sql_query (sql_oauth_consumer_name, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_consumer
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+ oauth_consumer_by_callback (a_callback: READABLE_STRING_8): detachable CMS_OAUTH_20_CONSUMER
+ -- Retrieve a consumer by callback `a_callback', if any.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".oauth_consumer_by_callback")
+ create l_parameters.make (1)
+ l_parameters.put (a_callback, "name")
+ sql_query (sql_oauth_consumer_callback, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_consumer
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+feature -- Change: User OAuth
+
+ new_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_GENERAL)
+ -- Add a new user with oauth2 authentication.
+ -- .
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ sql_begin_transaction
+
+ write_information_log (generator + ".new_user_oauth2")
+ create l_parameters.make (4)
+ l_parameters.put (a_user.id, "uid")
+ l_parameters.put (a_token, "token")
+ l_parameters.put (a_user_profile, "profile")
+ l_parameters.put (create {DATE_TIME}.make_now_utc, "utc_date")
+ l_parameters.put (a_user.email, "email")
+
+
+ create l_string.make_from_string (sql_insert_oauth2_template)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_insert (l_string, l_parameters)
+ sql_commit_transaction
+ sql_finalize
+ end
+
+ update_user_oauth2 (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer: READABLE_STRING_GENERAL )
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ sql_begin_transaction
+
+ write_information_log (generator + ".new_user_oauth2")
+ create l_parameters.make (4)
+ l_parameters.put (a_user.id, "uid")
+ l_parameters.put (a_token, "token")
+ l_parameters.put (a_user_profile, "profile")
+
+ create l_string.make_from_string (sql_update_oauth2_template)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_modify (l_string, l_parameters)
+ sql_commit_transaction
+ sql_finalize
+ end
+
+ remove_user_oauth2 (a_user: CMS_USER; a_consumer: READABLE_STRING_GENERAL)
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ l_string: STRING
+ do
+ error_handler.reset
+ sql_begin_transaction
+
+ write_information_log (generator + ".remove_user_oauth2")
+ create l_parameters.make (1)
+ l_parameters.put (a_user.id, "uid")
+
+ create l_string.make_from_string (sql_remove_oauth2_template)
+ l_string.replace_substring_all ("$table_name", oauth2_sql_table_name (a_consumer))
+ sql_modify (l_string, l_parameters)
+ sql_commit_transaction
+ sql_finalize
+ end
+
+feature {NONE} -- Implementation OAuth Consumer
+
+ fetch_consumer: detachable CMS_OAUTH_20_CONSUMER
+ do
+ if attached sql_read_integer_64 (1) as l_id then
+ create Result.make_with_id (l_id)
+
+ if attached sql_read_string (2) as l_name then
+ Result.set_name (l_name)
+ end
+ if attached sql_read_string (3) as l_api_secret then
+ Result.set_api_secret (l_api_secret)
+ end
+ if attached sql_read_string (4) as l_api_key then
+ Result.set_api_key (l_api_key)
+ end
+ if attached sql_read_string (5) as l_scope then
+ Result.set_scope (l_scope)
+ end
+ if attached sql_read_string (6) as l_resource_url then
+ Result.set_protected_resource_url (l_resource_url)
+ end
+ if attached sql_read_string (7) as l_callback_name then
+ Result.set_callback_name (l_callback_name)
+ end
+ if attached sql_read_string (8) as l_extractor then
+ Result.set_extractor (l_extractor)
+ end
+ if attached sql_read_string (9) as l_authorize_url then
+ Result.set_authorize_url (l_authorize_url)
+ end
+ if attached sql_read_string (10) as l_endpoint then
+ Result.set_endpoint (l_endpoint)
+ end
+ end
+ end
+
+feature {NONE} -- Implementation: User
+
+ fetch_user: detachable CMS_USER
+ local
+ l_id: INTEGER_64
+ l_name: detachable READABLE_STRING_32
+ do
+ if attached sql_read_integer_64 (1) as i then
+ l_id := i
+ end
+ if attached sql_read_string_32 (2) as s and then not s.is_whitespace then
+ l_name := s
+ end
+
+ if l_name /= Void then
+ create Result.make (l_name)
+ if l_id > 0 then
+ Result.set_id (l_id)
+ end
+ elseif l_id > 0 then
+ create Result.make_with_id (l_id)
+ end
+
+ if Result /= Void then
+ if attached sql_read_string (3) as l_password then
+ -- FIXME: should we return the password here ???
+ Result.set_hashed_password (l_password)
+ end
+ if attached sql_read_string (5) as l_email then
+ Result.set_email (l_email)
+ end
+ if attached sql_read_integer_32 (6) as l_status then
+ Result.set_status (l_status)
+ end
+ else
+ check expected_valid_user: False end
+ end
+ end
+
+feature {NONE} -- User OAuth2
+
+ oauth2_sql_table_name (a_consumer: READABLE_STRING_GENERAL): STRING_8
+ local
+ i,n: INTEGER
+ do
+ create Result.make_from_string (Sql_oauth2_table_prefix)
+ if a_consumer.is_valid_as_string_8 then
+ Result.append (a_consumer.to_string_8)
+ else
+ check only_ascii: False end
+ -- Replace non ascii char by '-'
+ from
+ i := 1
+ n := a_consumer.count
+ until
+ i > n
+ loop
+ if a_consumer [i].is_character_8 then
+ Result.append_code (a_consumer.code (i))
+ else
+ Result.append_character ('-')
+ end
+ i := i + 1
+ end
+ end
+ end
+
+ Select_user_by_oauth2_template_token: STRING = "SELECT u.* FROM users as u JOIN $table_name as og ON og.uid = u.uid and og.access_token = :token;"
+ --| FIXME: replace the u.* by a list of field names, to avoid breaking `featch_user' if two fieds are swiped.
+
+ Select_user_oauth2_template_by_id: STRING = "SELECT u.* FROM users as u JOIN $table_name as og ON og.uid = u.uid and og.uid = :uid;"
+
+ Select_user_oauth2_template_by_email: STRING = "SELECT u.* FROM users as u JOIN $table_name as og ON og.uid = u.uid and og.email = :email;"
+
+ Sql_insert_oauth2_template: STRING = "INSERT INTO $table_name (uid, access_token, details, created, email) VALUES (:uid, :token, :profile, :utc_date, :email);"
+
+ Sql_update_oauth2_template: STRING = "UPDATE $table_name SET access_token = :token, details = :profile WHERE uid =:uid;"
+
+ Sql_remove_oauth2_template: STRING = "DELETE FROM $table_name WHERE uid =:uid;"
+
+ Sql_oauth_consumers: STRING = "SELECT name FROM oauth2_consumers;"
+
+ Sql_oauth2_table_prefix: STRING = "oauth2_"
+
+feature {NONE} -- Consumer
+
+ Sql_oauth_consumer_callback: STRING = "SELECT * FROM oauth2_consumers where callback_name =:name;"
+
+ Sql_oauth_consumer_name: STRING = "SELECT * FROM oauth2_consumers where name =:name;"
+
+end
diff --git a/modules/oauth20/site/scripts/oauth2_consumers.sql b/modules/oauth20/site/scripts/oauth2_consumers.sql
new file mode 100644
index 0000000..1c7eea6
--- /dev/null
+++ b/modules/oauth20/site/scripts/oauth2_consumers.sql
@@ -0,0 +1,18 @@
+
+CREATE TABLE oauth2_consumers(
+ `cid` INTEGER PRIMARY KEY NOT NULL CHECK(`cid`>=0),
+ `name` VARCHAR(255) NOT NULL,
+ `api_secret` TEXT NOT NULL,
+ `api_key` TEXT NOT NULL,
+ `scope` VARCHAR (100) NOT NULL,
+ `protected_resource_url` VARCHAR (255) NOT NULL,
+ `callback_name` VARCHAR(255) NOT NULL,
+ `extractor` VARCHAR(50) NOT NULL,
+ `authorize_url` VARCHAR (255) NOT NULL,
+ `endpoint` VARCHAR (255) NOT NULL,
+ CONSTRAINT `cid`
+ UNIQUE(`cid`),
+ CONSTRAINT `name`
+ UNIQUE(`name`)
+ );
+
diff --git a/modules/oauth20/site/scripts/oauth2_consumers_initialize.sql b/modules/oauth20/site/scripts/oauth2_consumers_initialize.sql
new file mode 100644
index 0000000..a600e71
--- /dev/null
+++ b/modules/oauth20/site/scripts/oauth2_consumers_initialize.sql
@@ -0,0 +1,7 @@
+ -- Change the values TO_COMPLETE based on your API.
+ -- API SECTET KEY AND API PUBLIC KEY
+INSERT INTO oauth2_consumers (name, api_secret, api_key, scope, protected_resource_url, callback_name, extractor, authorize_url, endpoint)
+VALUES ('google', 'TO-COMPLETE', 'TO-COMPLETE', 'email', 'https://www.googleapis.com/plus/v1/people/me', 'callback_google', 'json','https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI','https://accounts.google.com/o/oauth2/token');
+
+INSERT INTO oauth2_consumers (name, api_secret, api_key, scope, protected_resource_url, callback_name, extractor, authorize_url, endpoint )
+VALUES ('facebook', 'TO-COMPLETE', 'TO-COMPLETE', 'email', 'https://graph.facebook.com/me', 'callback_facebook','text','https://www.facebook.com/dialog/oauth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI','https://graph.facebook.com/oauth/access_token');
diff --git a/modules/oauth20/site/scripts/oauth2_table.sql.tpl b/modules/oauth20/site/scripts/oauth2_table.sql.tpl
new file mode 100644
index 0000000..925a606
--- /dev/null
+++ b/modules/oauth20/site/scripts/oauth2_table.sql.tpl
@@ -0,0 +1,13 @@
+
+CREATE TABLE $table_name (
+ `uid` INTEGER PRIMARY KEY NOT NULL CHECK(`uid`>=0),
+ `access_token` TEXT NOT NULL,
+ `created` DATETIME NOT NULL,
+ `details` TEXT NOT NULL,
+ `email` TEXT NOT NULL,
+ CONSTRAINT `uid`
+ UNIQUE(`uid`),
+ CONSTRAINT `email`
+ UNIQUE(`email`)
+ );
+
diff --git a/modules/oauth20/site/templates/block_account_info.tpl b/modules/oauth20/site/templates/block_account_info.tpl
new file mode 100644
index 0000000..4a4c7ef
--- /dev/null
+++ b/modules/oauth20/site/templates/block_account_info.tpl
@@ -0,0 +1,32 @@
+
+ {unless isempty="$oauth_associated"}
+ Un-Associate Account with Oauth Consumer
+
+ {foreach item="consumer" from="$oauth_associated"}
+
+ {/foreach}
+
+ {/unless}
+ {unless isempty="$oauth_not_associated"}
+ Associate Account with Oauth Consumer
+
+ {foreach item="consumer" from="$oauth_not_associated"}
+
+ {/foreach}
+
+ {/unless}
diff --git a/modules/oauth20/site/templates/block_login.tpl b/modules/oauth20/site/templates/block_login.tpl
new file mode 100644
index 0000000..44c7bd4
--- /dev/null
+++ b/modules/oauth20/site/templates/block_login.tpl
@@ -0,0 +1,7 @@
+
diff --git a/modules/openid/cms_openid_api.e b/modules/openid/cms_openid_api.e
new file mode 100644
index 0000000..2e9a78e
--- /dev/null
+++ b/modules/openid/cms_openid_api.e
@@ -0,0 +1,72 @@
+note
+ description: "[
+ API to manage CMS User Openid authentication.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_API
+inherit
+ CMS_MODULE_API
+
+ REFACTORING_HELPER
+
+create {CMS_OPENID_MODULE}
+ make_with_storage
+
+feature {NONE} -- Initialization
+
+ make_with_storage (a_api: CMS_API; a_openid_storage: CMS_OPENID_STORAGE_I)
+ -- Create an object with api `a_api' and storage `a_openid_storage'.
+ do
+ openid_storage := a_openid_storage
+ make (a_api)
+ ensure
+ openid_storage_set: openid_storage = a_openid_storage
+ end
+
+feature {CMS_MODULE} -- Access: User openid storage.
+
+ openid_storage: CMS_OPENID_STORAGE_I
+ -- storage interface.
+
+feature -- Access: User Openid
+
+ user_openid_by_userid_identity (a_uid: like {CMS_USER}.id; a_identity: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by id `a_uid' with identity `a_identity', if any.
+ do
+ Result := openid_storage.user_openid_by_userid_identity (a_uid, a_identity)
+ end
+
+ user_openid_by_identity (a_identity: READABLE_STRING_GENERAL): detachable CMS_USER
+ do
+ Result := openid_storage.user_openid_by_identity (a_identity)
+ end
+
+feature -- Access: Consumers OAuth20
+
+ openid_consumers: LIST [STRING]
+ -- List of Openid consumers, if any, empty in other case.
+ do
+ Result := openid_storage.openid_consumers
+ end
+
+ openid_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OPENID_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ do
+ Result := openid_storage.openid_consumer_by_name (a_name)
+ end
+
+feature -- Change: User Openid
+
+
+ new_user_openid (a_identity: READABLE_STRING_GENERAL; a_user: CMS_USER)
+ -- Add a new user with openid using the identity `a_identity'.
+ require
+ has_id: a_user.has_id
+ do
+ openid_storage.new_user_openid (a_identity,a_user)
+ end
+
+end
diff --git a/modules/openid/cms_openid_constants.e b/modules/openid/cms_openid_constants.e
new file mode 100644
index 0000000..9269e19
--- /dev/null
+++ b/modules/openid/cms_openid_constants.e
@@ -0,0 +1,16 @@
+note
+ description: "Summary description for {CMS_OPENID_CONSTANTS}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_CONSTANTS
+
+feature -- Access
+
+ openid_session: STRING = "EWF_ROC_OPENID_TOKEN_"
+ -- Name of Cookie used to keep the session info.
+ -- FIXME: make this configurable.
+
+ consumer: STRING = "consumer"
+end
diff --git a/modules/openid/cms_openid_consumer.e b/modules/openid/cms_openid_consumer.e
new file mode 100644
index 0000000..214b3ba
--- /dev/null
+++ b/modules/openid/cms_openid_consumer.e
@@ -0,0 +1,71 @@
+note
+ description: "Summary description for {CMS_OPENID_CONSUMER}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_CONSUMER
+
+inherit
+ ANY
+ redefine
+ default_create
+ end
+
+create
+ default_create,
+ make_with_id
+
+feature {NONE} -- Initialization
+
+ make_with_id (a_id: like id)
+ do
+ id := a_id
+ default_create
+ end
+
+ default_create
+ do
+ set_endpoint ("")
+ set_name ("")
+ end
+
+feature -- Access
+
+ endpoint: READABLE_STRING_8
+ -- Url to authorize the user.
+
+ name: READABLE_STRING_8
+ -- consumer name.
+
+ id: INTEGER_64
+ -- unique identifier.
+
+feature -- Element change
+
+
+ set_endpoint (a_endpoint: like endpoint)
+ -- Assign `endpoint' with `a_endpoint'.
+ do
+ endpoint := a_endpoint
+ ensure
+ endpoint_assigned: endpoint = a_endpoint
+ end
+
+ set_name (a_name: like name)
+ -- Assign `name' with `a_name'.
+ do
+ name := a_name
+ ensure
+ name_assigned: name = a_name
+ end
+
+ set_id (an_id: like id)
+ -- Assign `id' with `an_id'.
+ do
+ id := an_id
+ ensure
+ id_assigned: id = an_id
+ end
+
+end
diff --git a/modules/openid/cms_openid_module.e b/modules/openid/cms_openid_module.e
new file mode 100644
index 0000000..cc84735
--- /dev/null
+++ b/modules/openid/cms_openid_module.e
@@ -0,0 +1,519 @@
+note
+ description: "[
+ Generic OpenID Module supporting authentication using different providers.
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_MODULE
+
+inherit
+ CMS_MODULE
+ rename
+ module_api as user_openid_api
+ redefine
+ filters,
+ setup_hooks,
+ initialize,
+ install,
+ user_openid_api
+ end
+
+
+ CMS_HOOK_BLOCK
+
+ CMS_HOOK_AUTO_REGISTER
+
+ CMS_HOOK_MENU_SYSTEM_ALTER
+
+ CMS_HOOK_VALUE_TABLE_ALTER
+
+ SHARED_EXECUTION_ENVIRONMENT
+ export
+ {NONE} all
+ end
+
+ REFACTORING_HELPER
+
+ SHARED_LOGGER
+
+ CMS_REQUEST_UTIL
+
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make
+ -- Create current module
+ do
+ version := "1.0"
+ description := "Openid module"
+ package := "authentication"
+ add_dependency ({CMS_AUTHENTICATION_MODULE})
+
+ create root_dir.make_current
+ cache_duration := 0
+ end
+
+feature -- Access
+
+ name: STRING = "openid"
+ --
+
+feature {CMS_API} -- Module Initialization
+
+ initialize (a_api: CMS_API)
+ --
+ local
+ l_openid_api: like user_openid_api
+ l_openid_storage: CMS_OPENID_STORAGE_I
+ do
+ Precursor (a_api)
+
+ -- Storage initialization
+ if attached a_api.storage.as_sql_storage as l_storage_sql then
+ create {CMS_OPENID_STORAGE_SQL} l_openid_storage.make (l_storage_sql)
+ else
+ -- FIXME: in case of NULL storage, should Current be disabled?
+ create {CMS_OPENID_STORAGE_NULL} l_openid_storage
+ end
+
+ -- API initialization
+ create l_openid_api.make_with_storage (a_api, l_openid_storage)
+ user_openid_api := l_openid_api
+ ensure then
+ user_opend_api_set: user_openid_api /= Void
+ end
+
+feature {CMS_API} -- Module management
+
+ install (api: CMS_API)
+ do
+ -- Schema
+ if attached api.storage.as_sql_storage as l_sql_storage then
+ if not l_sql_storage.sql_table_exists ("openid_consumers") then
+ --| Schema
+ l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("openid_consumers.sql")), Void)
+
+ if l_sql_storage.has_error then
+ api.logger.put_error ("Could not initialize database for openid module", generating_type)
+ end
+ -- TODO workaround.
+ l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("openid_consumers_initialize.sql")), Void)
+ end
+
+ -- TODO workaround, until we have an admin module
+ if l_sql_storage.has_error then
+ api.logger.put_error ("Could not initialize database for different consumers", generating_type)
+ else
+ l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("openid_items.sql")),Void)
+ end
+ Precursor {CMS_MODULE}(api)
+ end
+ end
+
+feature {CMS_API} -- Access: API
+
+ user_openid_api: detachable CMS_OPENID_API
+ --
+
+feature -- Filters
+
+ filters (a_api: CMS_API): detachable LIST [WSF_FILTER]
+ -- Possibly list of Filter's module.
+ do
+ if attached user_openid_api as l_user_openid_api then
+ create {ARRAYED_LIST [WSF_FILTER]} Result.make (1)
+ Result.extend (create {CMS_OPENID_FILTER}.make (a_api, l_user_openid_api))
+ end
+ end
+
+feature -- Access: docs
+
+ root_dir: PATH
+
+ cache_duration: INTEGER
+ -- Caching duration
+ --| 0: disable
+ --| -1: cache always valie
+ --| nb: cache expires after nb seconds.
+
+ cache_disabled: BOOLEAN
+ do
+ Result := cache_duration = 0
+ end
+
+feature -- Router
+
+ setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
+ --
+ do
+ if attached user_openid_api as l_user_openid_api then
+ configure_web (a_api, l_user_openid_api, a_router)
+ end
+ end
+
+ configure_web (a_api: CMS_API; a_user_openid_api: CMS_OPENID_API; a_router: WSF_ROUTER)
+ do
+ a_router.handle ("/account/roc-openid-login", create {WSF_URI_AGENT_HANDLER}.make (agent handle_openid_login (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/roc-openid-logout", create {WSF_URI_AGENT_HANDLER}.make (agent handle_logout (a_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/login-with-openid/{consumer}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_login_with_openid (a_api,a_user_openid_api, ?, ?)), a_router.methods_get_post)
+ a_router.handle ("/account/openid-callback", create {WSF_URI_AGENT_HANDLER}.make (agent handle_callback_openid (a_api, a_user_openid_api, ?, ?)), a_router.methods_get_post)
+ end
+
+feature -- Hooks configuration
+
+ setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
+ -- Module hooks configuration.
+ do
+ auto_subscribe_to_hooks (a_hooks)
+ a_hooks.subscribe_to_block_hook (Current)
+ a_hooks.subscribe_to_value_table_alter_hook (Current)
+ end
+
+feature -- Hooks
+
+ value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE)
+ --
+ do
+ if
+ attached a_response.user as u and then
+ attached {WSF_STRING} a_response.request.cookie ({CMS_OPENID_CONSTANTS}.openid_session)
+ then
+ a_value.force ("account/roc-openid-logout", "auth_login_strategy")
+ end
+ end
+
+ menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
+ -- Hook execution on collection of menu contained by `a_menu_system'
+ -- for related response `a_response'.
+ local
+ lnk: CMS_LOCAL_LINK
+ lnk2: detachable CMS_LINK
+ do
+ if
+ attached a_response.user as u and then
+ attached {WSF_STRING} a_response.request.cookie ({CMS_OPENID_CONSTANTS}.openid_session) as l_roc_auth_session_token
+ then
+ across
+ a_menu_system.primary_menu.items as ic
+ until
+ lnk2 /= Void
+ loop
+ if
+ ic.item.location.same_string ("account/roc-logout") or else
+ ic.item.location.same_string ("basic_auth_logoff")
+ then
+ lnk2 := ic.item
+ end
+ end
+ if lnk2 /= Void then
+ a_menu_system.primary_menu.remove (lnk2)
+ end
+ create lnk.make ("Logout", "account/roc-openid-logout" )
+ a_menu_system.primary_menu.extend (lnk)
+ else
+ if a_response.location.starts_with ("account/") then
+ create lnk.make ("Openid", "account/roc-openid-login")
+ a_response.add_to_primary_tabs (lnk)
+ end
+ end
+
+ end
+
+ block_list: ITERABLE [like {CMS_BLOCK}.name]
+ local
+ l_string: STRING
+ do
+ Result := <<"login">>
+ debug ("roc")
+ create l_string.make_empty
+ across
+ Result as ic
+ loop
+ l_string.append (ic.item)
+ l_string.append_character (' ')
+ end
+ write_debug_log (generator + ".block_list:" + l_string )
+ end
+ end
+
+ get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ do
+ if
+ a_block_id.is_case_insensitive_equal_general ("login") and then
+ a_response.location.starts_with ("account/roc-openid-login")
+ then
+ get_block_view_login (a_block_id, a_response)
+ end
+ end
+
+ handle_openid_login (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ o: OPENID_CONSUMER
+ s: STRING
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ if req.is_get_request_method then
+ r.set_value ("Login", "optional_content_type")
+ r.execute
+ elseif req.is_post_request_method then
+ create s.make_empty
+ if attached req.string_item ("openid") as p_openid then
+ s.append ("Check openID: " + p_openid)
+ create o.make (req.absolute_script_url ("/account/login-with-openid"))
+ o.ask_email (True)
+ o.ask_all_info (False)
+ if attached o.auth_url (p_openid) as l_url then
+ r.set_redirection (l_url)
+ else
+ s.append (" Failure")
+ r.set_status_code ({HTTP_CONSTANTS}.bad_request)
+ r.values.force (s, "error")
+ r.execute
+ end
+ end
+ end
+ end
+
+ handle_logout (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_cookie: WSF_COOKIE
+ do
+ if
+ attached {WSF_STRING} req.cookie ({CMS_OPENID_CONSTANTS}.openid_session) as l_cookie_token and then
+ attached {CMS_USER} current_user (req) as l_user
+ then
+ -- Logout OAuth
+ create l_cookie.make ({CMS_OPENID_CONSTANTS}.openid_session, l_cookie_token.value)
+ l_cookie.set_path ("/")
+ l_cookie.set_max_age (-1)
+ res.add_cookie (l_cookie)
+ unset_current_user (req)
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ r.set_status_code ({HTTP_CONSTANTS}.found)
+ r.set_redirection (req.absolute_script_url (""))
+ r.execute
+ else
+ -- FIXME: missing implementation!
+ end
+ end
+
+feature {NONE} -- Helpers
+
+ template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK
+ -- Smarty content block for `a_block_id'
+ local
+ p: detachable PATH
+ do
+ create p.make_from_string ("templates")
+ p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl")
+ p := a_response.api.module_theme_resource_location (Current, p)
+ if p /= Void then
+ if attached p.entry as e then
+ create Result.make (a_block_id, Void, p.parent, e)
+ else
+ create Result.make (a_block_id, Void, p.parent, p)
+ end
+ end
+ end
+
+feature {NONE} -- Block views
+
+ get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
+ local
+ vals: CMS_VALUE_TABLE
+ do
+ if attached template_block (a_block_id, a_response) as l_tpl_block then
+ create vals.make (1)
+ -- add the variable to the block
+ value_table_alter (vals, a_response)
+ across
+ vals as ic
+ loop
+ l_tpl_block.set_value (ic.item, ic.key)
+ end
+ if
+ attached user_openid_api as l_openid_api and then
+ attached l_openid_api.openid_consumers as l_list
+ then
+ l_tpl_block.set_value (l_list, "openid_consumers")
+ end
+
+ a_response.add_block (l_tpl_block, "content")
+ else
+ debug ("cms")
+ a_response.add_warning_message ("Error with block [" + a_block_id + "]")
+ end
+ end
+ end
+
+
+feature -- Openid Login
+
+ handle_login_with_openid (api: CMS_API; a_oauth_api: CMS_OPENID_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ b: STRING
+ o: OPENID_CONSUMER
+ do
+ if attached {WSF_STRING} req.path_parameter ({CMS_OPENID_CONSTANTS}.consumer) as p_openid and then
+ attached {CMS_OPENID_CONSUMER} a_oauth_api.openid_consumer_by_name (p_openid.value) as l_oc then
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ create b.make_empty
+ b.append ("Check openID: " + p_openid.value)
+ create o.make (req.absolute_script_url ("/account/openid-callback"))
+ o.ask_email (True)
+ o.ask_all_info (False)
+ if attached o.auth_url (l_oc.endpoint) as l_url then
+ r.set_redirection (l_url)
+ else
+ b.append ("Failure")
+ end
+ r.execute
+ else
+ create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api)
+ r.set_main_content ("Bad request")
+ r.execute
+ end
+ end
+
+ handle_callback_openid (api: CMS_API; a_user_openid_api: CMS_OPENID_API; req: WSF_REQUEST; res: WSF_RESPONSE)
+ local
+ r: CMS_RESPONSE
+ l_user_api: CMS_USER_API
+ l_user: CMS_USER
+ l_roles: LIST [CMS_USER_ROLE]
+ l_cookie: WSF_COOKIE
+ es: CMS_AUTHENTICATON_EMAIL_SERVICE
+ b: STRING
+ o: OPENID_CONSUMER
+ v: OPENID_CONSUMER_VALIDATION
+ do
+ create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
+ create b.make_empty
+ if attached req.string_item ("openid.mode") as l_openid_mode then
+ create o.make (req.absolute_script_url ("/"))
+ o.ask_email (True)
+ o.ask_nickname (False)
+ create v.make_from_items (o, req.items_as_string_items)
+ v.validate
+ if v.is_valid then
+ if attached v.identity as l_identity and then
+ attached v.email_attribute as l_email
+ then
+ l_user_api := api.user_api
+ if attached l_user_api.user_by_email (l_email) as p_user then
+ -- User with email exist
+ if attached a_user_openid_api.user_openid_by_userid_identity (p_user.id, l_identity) then
+ -- Update openid entry?
+ else
+ -- create a oauth entry
+ a_user_openid_api.new_user_openid (l_identity,p_user)
+ end
+ create l_cookie.make ({CMS_OPENID_CONSTANTS}.openid_session, l_identity)
+ l_cookie.set_max_age (3600)
+ l_cookie.set_path ("/")
+ res.add_cookie (l_cookie)
+ else
+
+ create {ARRAYED_LIST [CMS_USER_ROLE]} l_roles.make (1)
+ l_roles.force (l_user_api.authenticated_user_role)
+
+ -- Create a new user and oauth entry
+ create l_user.make (l_email)
+ l_user.set_email (l_email)
+ l_user.set_password (new_token) -- generate a random password.
+ l_user.set_roles (l_roles)
+ l_user.mark_active
+ l_user_api.new_user (l_user)
+
+ -- Add oauth entry
+ a_user_openid_api.new_user_openid (l_identity, l_user )
+ create l_cookie.make ({CMS_OPENID_CONSTANTS}.openid_session, l_identity)
+ l_cookie.set_max_age (3600)
+ l_cookie.set_path ("/")
+ res.add_cookie (l_cookie)
+
+ -- Send Email
+ create es.make (create {CMS_AUTHENTICATION_EMAIL_SERVICE_PARAMETERS}.make (api))
+ write_debug_log (generator + ".handle_callback_openid: send_contact_welcome_email")
+ es.send_contact_welcome_email (l_email, "")
+ end
+ end
+ r.set_redirection (r.front_page_url)
+ r.execute
+ else
+ b.append ("User authentication failed!!")
+ end
+ end
+ end
+
+feature {NONE} -- Token Generation
+
+ new_token: STRING
+ -- Generate a new token activation token
+ local
+ l_token: STRING
+ l_security: SECURITY_PROVIDER
+ l_encode: URL_ENCODER
+ do
+ create l_security
+ l_token := l_security.token
+ create l_encode
+ from until l_token.same_string (l_encode.encoded_string (l_token)) loop
+ -- Loop ensure that we have a security token that does not contain characters that need encoding.
+ -- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token
+ -- but the user will need to use an unencoded token if activation has to be done manually.
+ l_token := l_security.token
+ end
+ Result := l_token
+ end
+
+feature {NONE} -- Implementation: date and time
+
+ http_date_format_to_date (s: READABLE_STRING_8): detachable DATE_TIME
+ local
+ d: HTTP_DATE
+ do
+ create d.make_from_string (s)
+ if not d.has_error then
+ Result := d.date_time
+ end
+ end
+
+ file_date (p: PATH): DATE_TIME
+ require
+ path_exists: (create {FILE_UTILITIES}).file_path_exists (p)
+ local
+ f: RAW_FILE
+ do
+ create f.make_with_path (p)
+ Result := timestamp_to_date (f.date)
+ end
+
+ timestamp_to_date (n: INTEGER): DATE_TIME
+ local
+ d: HTTP_DATE
+ do
+ create d.make_from_timestamp (n)
+ Result := d.date_time
+ end
+
+
+note
+ copyright: "Copyright (c) 1984-2013, Eiffel Software and others"
+ license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
+ source: "[
+ Eiffel Software
+ 5949 Hollister Ave., Goleta, CA 93117 USA
+ Telephone 805-685-1006, Fax 805-685-6869
+ Website http://www.eiffel.com
+ Customer support http://support.eiffel.com
+ ]"
+end
diff --git a/modules/openid/filter/cms_openid_filter.e b/modules/openid/filter/cms_openid_filter.e
new file mode 100644
index 0000000..277ab37
--- /dev/null
+++ b/modules/openid/filter/cms_openid_filter.e
@@ -0,0 +1,55 @@
+note
+ description: "[
+ Extracts an Openid token from the incoming request (cookie) and uses it to populate the user (or cms user context)
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_FILTER
+
+inherit
+ WSF_URI_TEMPLATE_HANDLER
+ CMS_HANDLER
+ rename
+ make as make_handler
+ end
+
+ WSF_FILTER
+
+create
+ make
+
+feature {NONE} -- Initialization
+
+ make (a_api: CMS_API; a_user_openid_api: CMS_OPENID_API)
+ do
+ make_handler (a_api)
+ user_openid_api := a_user_openid_api
+ end
+
+ user_openid_api: CMS_OPENID_API
+
+feature -- Basic operations
+
+ execute (req: WSF_REQUEST; res: WSF_RESPONSE)
+ -- Execute the filter.
+
+ do
+ api.logger.put_debug (generator + ".execute ", Void)
+ -- A valid user
+ if
+ attached {WSF_STRING} req.cookie ({CMS_OPENID_CONSTANTS}.openid_session) as l_roc_openid_session_token
+ then
+ if attached user_openid_api.user_openid_by_identity (l_roc_openid_session_token.value) as l_user then
+ set_current_user (req, l_user)
+ else
+ api.logger.put_error (generator + ".execute login_valid failed for: " + l_roc_openid_session_token.value , Void)
+ end
+ else
+ api.logger.put_debug (generator + ".execute without authentication", Void)
+ end
+ execute_next (req, res)
+ end
+
+end
diff --git a/modules/openid/openid-safe.ecf b/modules/openid/openid-safe.ecf
new file mode 100644
index 0000000..9bbfb18
--- /dev/null
+++ b/modules/openid/openid-safe.ecf
@@ -0,0 +1,28 @@
+
+
+
+
+
+ /.git$
+ /EIFGENs$
+ /.svn$
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/openid/persitence/cms_openid_storage_i.e b/modules/openid/persitence/cms_openid_storage_i.e
new file mode 100644
index 0000000..bb0ae47
--- /dev/null
+++ b/modules/openid/persitence/cms_openid_storage_i.e
@@ -0,0 +1,54 @@
+note
+ description: "[
+ API to handle Openid storage
+ ]"
+ date: "$Date$"
+ revision: "$Revision$"
+
+deferred class
+ CMS_OPENID_STORAGE_I
+
+inherit
+ SHARED_LOGGER
+
+feature -- Error Handling
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ deferred
+ end
+
+feature -- Access: Users
+
+ user_openid_by_userid_identity (a_uid: like {CMS_USER}.id; a_consumer_table: READABLE_STRING_GENERAL): detachable CMS_USER
+ -- Retrieve a user by id `a_uid' for the consumer `a_consumer', if aby.
+ deferred
+ end
+
+ user_openid_by_identity (a_identity: READABLE_STRING_GENERAL;): detachable CMS_USER
+ -- Retrieve a user by identity `a_identity'.
+ deferred
+ end
+
+feature -- Access: Consumers
+
+ openid_consumers: LIST [STRING]
+ -- Return a list of consumers, or empty
+ deferred
+ end
+
+ openid_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OPENID_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ deferred
+ end
+
+feature -- Change: User Oauth2
+
+ new_user_openid (a_identity: READABLE_STRING_GENERAL; a_user: CMS_USER)
+ -- Add a new user with openid authentication.
+ deferred
+ end
+
+
+
+end
diff --git a/modules/openid/persitence/cms_openid_storage_null.e b/modules/openid/persitence/cms_openid_storage_null.e
new file mode 100644
index 0000000..bd552da
--- /dev/null
+++ b/modules/openid/persitence/cms_openid_storage_null.e
@@ -0,0 +1,60 @@
+note
+ description: "Summary description for {CMS_OPENID_STORAGE_NULL}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_STORAGE_NULL
+
+inherit
+
+ CMS_OPENID_STORAGE_I
+
+
+feature -- Error handler
+
+ error_handler: ERROR_HANDLER
+ -- Error handler.
+ do
+ create Result.make
+ end
+
+feature -- Access: Users
+
+ user_openid_by_userid_identity (a_uid: like {CMS_USER}.id; a_identity: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ do
+ end
+
+ user_openid_by_identity (a_identity: READABLE_STRING_GENERAL;): detachable CMS_USER
+ --
+ do
+ end
+
+feature -- Access: Consumers
+
+ openid_consumers: LIST [STRING]
+ --
+ do
+ create {ARRAYED_LIST[STRING]}Result.make(0)
+ end
+
+ openid_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OPENID_CONSUMER
+ --
+ do
+ end
+
+feature -- Change: User Oauth2
+
+ new_user_openid (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER)
+ --
+ do
+ end
+
+ update_user_openid (a_token: READABLE_STRING_GENERAL; a_user_profile: READABLE_STRING_32; a_user: CMS_USER; a_consumer_table: READABLE_STRING_GENERAL )
+ -- Update user `a_user' with oauth2 authentication.
+ do
+ end
+
+
+end
diff --git a/modules/openid/persitence/cms_openid_storage_sql.e b/modules/openid/persitence/cms_openid_storage_sql.e
new file mode 100644
index 0000000..bb2f0d7
--- /dev/null
+++ b/modules/openid/persitence/cms_openid_storage_sql.e
@@ -0,0 +1,207 @@
+note
+ description: "Summary description for {CMS_OPENID_STORAGE_SQL}."
+ date: "$Date$"
+ revision: "$Revision$"
+
+class
+ CMS_OPENID_STORAGE_SQL
+
+inherit
+ CMS_OPENID_STORAGE_I
+
+ CMS_PROXY_STORAGE_SQL
+
+ CMS_OPENID_STORAGE_I
+
+ CMS_STORAGE_SQL_I
+
+ REFACTORING_HELPER
+
+create
+ make
+
+feature -- Access User Outh
+
+ user_openid_by_userid_identity (a_uid: like {CMS_USER}.id; a_identity: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_openid_by_userid_identity")
+ create l_parameters.make (1)
+ l_parameters.put (a_uid, "uid")
+ l_parameters.put (a_identity, "identity")
+ sql_query (Select_user_openid_by_id, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_user
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ end
+ sql_finalize
+ end
+
+ user_openid_by_identity (a_identity: READABLE_STRING_GENERAL): detachable CMS_USER
+ --
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".user_openid_by_identity")
+ create l_parameters.make (1)
+ l_parameters.put (a_identity, "identity")
+ sql_query (Select_user_by_openid_identity, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_user
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ Result := Void
+ end
+ else
+ check no_more_than_one: False end
+ end
+ sql_finalize
+ end
+
+feature --Access: Consumers
+
+ openid_consumers: LIST [STRING]
+ -- Return a list of consumers, or empty
+ do
+ error_handler.reset
+ create {ARRAYED_LIST [STRING]} Result.make (0)
+ write_information_log (generator + ".openid_consumers")
+ sql_query (Sql_openid_consumers, Void)
+ if not has_error then
+ from
+ sql_start
+ until
+ sql_after
+ loop
+ if attached sql_read_string (1) as l_name then
+ Result.force (l_name)
+ end
+ sql_forth
+ end
+ end
+ sql_finalize
+ end
+
+ openid_consumer_by_name (a_name: READABLE_STRING_8): detachable CMS_OPENID_CONSUMER
+ -- Retrieve a consumer by name `a_name', if any.
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ write_information_log (generator + ".openid_consumer_by_name")
+ create l_parameters.make (1)
+ l_parameters.put (a_name, "name")
+ sql_query (sql_openid_consumer_name, l_parameters)
+ if not has_error and not sql_after then
+ Result := fetch_consumer
+ sql_forth
+ if not sql_after then
+ check no_more_than_one: False end
+ end
+ end
+ sql_finalize
+ end
+
+feature -- Change: User OAuth
+
+ new_user_openid (a_identity: READABLE_STRING_GENERAL; a_user: CMS_USER)
+ -- Add a new user with openid authentication.
+ -- .
+ local
+ l_parameters: STRING_TABLE [detachable ANY]
+ do
+ error_handler.reset
+ sql_begin_transaction
+
+ write_information_log (generator + ".new_user_openid")
+ create l_parameters.make (4)
+ l_parameters.put (a_user.id, "uid")
+ l_parameters.put (a_identity, "identity")
+ l_parameters.put (create {DATE_TIME}.make_now_utc, "utc_date")
+ sql_insert (Sql_insert_openid, l_parameters)
+ sql_commit_transaction
+ sql_finalize
+ end
+
+feature {NONE} -- Implementation OAuth Consumer
+
+ fetch_consumer: detachable CMS_OPENID_CONSUMER
+ do
+ if attached sql_read_integer_64 (1) as l_id then
+ create Result.make_with_id (l_id)
+
+ if attached sql_read_string (2) as l_name then
+ Result.set_name (l_name)
+ end
+ if attached sql_read_string (3) as l_endpoint then
+ Result.set_endpoint (l_endpoint)
+ end
+ end
+ end
+
+feature {NONE} -- Implementation: User
+
+ fetch_user: detachable CMS_USER
+ local
+ l_id: INTEGER_64
+ l_name: detachable READABLE_STRING_32
+ do
+ if attached sql_read_integer_64 (1) as i then
+ l_id := i
+ end
+ if attached sql_read_string_32 (2) as s and then not s.is_whitespace then
+ l_name := s
+ end
+
+ if l_name /= Void then
+ create Result.make (l_name)
+ if l_id > 0 then
+ Result.set_id (l_id)
+ end
+ elseif l_id > 0 then
+ create Result.make_with_id (l_id)
+ end
+
+ if Result /= Void then
+ if attached sql_read_string (3) as l_password then
+ -- FIXME: should we return the password here ???
+ Result.set_hashed_password (l_password)
+ end
+ if attached sql_read_string (5) as l_email then
+ Result.set_email (l_email)
+ end
+ if attached sql_read_integer_32 (6) as l_status then
+ Result.set_status (l_status)
+ end
+ else
+ check expected_valid_user: False end
+ end
+ end
+
+feature {NONE} -- User OpenID
+
+
+ Select_user_by_openid_identity: STRING = "SELECT u.* FROM users as u JOIN openid_items as og ON og.uid = u.uid and og.identity = :identity;"
+ --| FIXME: replace the u.* by a list of field names, to avoid breaking `featch_user' if two fieds are swiped.
+
+ Select_user_openid_by_id: STRING = "SELECT u.* FROM users as u JOIN openid_items as og ON og.uid = u.uid and og.uid = :uid and og.identity = :identity;"
+
+ Sql_insert_openid: STRING = "INSERT INTO openid_items (uid, identity, created) VALUES (:uid, :identity, :utc_date);"
+
+ Sql_openid_consumers: STRING = "SELECT name FROM openid_consumers;"
+
+
+feature {NONE} -- Consumer
+
+ Sql_openid_consumer_name: STRING = "SELECT * FROM openid_consumers where name =:name;"
+
+end
diff --git a/modules/openid/site/scripts/openid_consumers.sql b/modules/openid/site/scripts/openid_consumers.sql
new file mode 100644
index 0000000..b0f6368
--- /dev/null
+++ b/modules/openid/site/scripts/openid_consumers.sql
@@ -0,0 +1,11 @@
+
+CREATE TABLE openid_consumers(
+ `cid` INTEGER PRIMARY KEY NOT NULL CHECK(`cid`>=0),
+ `name` VARCHAR(255) NOT NULL,
+ `endpoint` VARCHAR (255) NOT NULL,
+ CONSTRAINT `cid`
+ UNIQUE(`cid`),
+ CONSTRAINT `name`
+ UNIQUE(`name`)
+ );
+
diff --git a/modules/openid/site/scripts/openid_consumers_initialize.sql b/modules/openid/site/scripts/openid_consumers_initialize.sql
new file mode 100644
index 0000000..dbd9efe
--- /dev/null
+++ b/modules/openid/site/scripts/openid_consumers_initialize.sql
@@ -0,0 +1,4 @@
+ -- Change the values TO_COMPLETE based on your API.
+ -- API SECTET KEY AND API PUBLIC KEY
+INSERT INTO openid_consumers (name, endpoint)
+VALUES ('yahoo', 'https://me.yahoo.com/');
diff --git a/modules/openid/site/scripts/openid_items.sql b/modules/openid/site/scripts/openid_items.sql
new file mode 100644
index 0000000..474f7cb
--- /dev/null
+++ b/modules/openid/site/scripts/openid_items.sql
@@ -0,0 +1,11 @@
+
+CREATE TABLE openid_items (
+ `uid` INTEGER PRIMARY KEY NOT NULL CHECK(`uid`>=0),
+ `identity` TEXT NOT NULL,
+ `created` DATETIME NOT NULL,
+ CONSTRAINT `uid`
+ UNIQUE(`uid`),
+ CONSTRAINT `identity`
+ UNIQUE(`identity`)
+ );
+
diff --git a/modules/openid/site/templates/block_login.tpl b/modules/openid/site/templates/block_login.tpl
new file mode 100644
index 0000000..4c35029
--- /dev/null
+++ b/modules/openid/site/templates/block_login.tpl
@@ -0,0 +1,18 @@
+