").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
diff --git a/docs/js/typeahead.min.js b/docs/js/typeahead.min.js
deleted file mode 100644
index 47c2f029..00000000
--- a/docs/js/typeahead.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*!
- * typeahead.js 0.10.5
- * https://github.com/twitter/typeahead.js
- * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
- */
-!function(t){var e=function(){"use strict";return{isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(t){return!t||/^\s*$/.test(t)},escapeRegExChars:function(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(t){return"string"==typeof t},isNumber:function(t){return"number"==typeof t},isArray:t.isArray,isFunction:t.isFunction,isObject:t.isPlainObject,isUndefined:function(t){return"undefined"==typeof t},toStr:function(t){return e.isUndefined(t)||null===t?"":t+""},bind:t.proxy,each:function(e,n){function i(t,e){return n(e,t)}t.each(e,i)},map:t.map,filter:t.grep,every:function(e,n){var i=!0;return e?(t.each(e,function(t,r){return(i=n.call(null,r,t,e))?void 0:!1}),!!i):i},some:function(e,n){var i=!1;return e?(t.each(e,function(t,r){return(i=n.call(null,r,t,e))?!1:void 0}),!!i):i},mixin:t.extend,getUniqueId:function(){var t=0;return function(){return t++}}(),templatify:function(e){function n(){return String(e)}return t.isFunction(e)?e:n},defer:function(t){setTimeout(t,0)},debounce:function(t,e,n){var i,r;return function(){var s,o,u=this,a=arguments;return s=function(){i=null,n||(r=t.apply(u,a))},o=n&&!i,clearTimeout(i),i=setTimeout(s,e),o&&(r=t.apply(u,a)),r}},throttle:function(t,e){var n,i,r,s,o,u;return o=0,u=function(){o=new Date,r=null,s=t.apply(n,i)},function(){var a=new Date,c=e-(a-o);return n=this,i=arguments,0>=c?(clearTimeout(r),r=null,o=a,s=t.apply(n,i)):r||(r=setTimeout(u,c)),s}},noop:function(){}}}(),n="0.10.5",i=function(){"use strict";function t(t){return t=e.toStr(t),t?t.split(/\s+/):[]}function n(t){return t=e.toStr(t),t?t.split(/\W+/):[]}function i(t){return function(){var n=[].slice.call(arguments,0);return function(i){var r=[];return e.each(n,function(n){r=r.concat(t(e.toStr(i[n])))}),r}}}return{nonword:n,whitespace:t,obj:{nonword:i(n),whitespace:i(t)}}}(),r=function(){"use strict";function n(n){this.maxSize=e.isNumber(n)?n:100,this.reset(),this.maxSize<=0&&(this.set=this.get=t.noop)}function i(){this.head=this.tail=null}function r(t,e){this.key=t,this.val=e,this.prev=this.next=null}return e.mixin(n.prototype,{set:function(t,e){var n,i=this.list.tail;this.size>=this.maxSize&&(this.list.remove(i),delete this.hash[i.key]),(n=this.hash[t])?(n.val=e,this.list.moveToFront(n)):(n=new r(t,e),this.list.add(n),this.hash[t]=n,this.size++)},get:function(t){var e=this.hash[t];return e?(this.list.moveToFront(e),e.val):void 0},reset:function(){this.size=0,this.hash={},this.list=new i}}),e.mixin(i.prototype,{add:function(t){this.head&&(t.next=this.head,this.head.prev=t),this.head=t,this.tail=this.tail||t},remove:function(t){t.prev?t.prev.next=t.next:this.head=t.next,t.next?t.next.prev=t.prev:this.tail=t.prev},moveToFront:function(t){this.remove(t),this.add(t)}}),n}(),s=function(){"use strict";function t(t){this.prefix=["__",t,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+e.escapeRegExChars(this.prefix))}function n(){return(new Date).getTime()}function i(t){return JSON.stringify(e.isUndefined(t)?null:t)}function r(t){return JSON.parse(t)}var s,o;try{s=window.localStorage,s.setItem("~~~","!"),s.removeItem("~~~")}catch(u){s=null}return o=s&&window.JSON?{_prefix:function(t){return this.prefix+t},_ttlKey:function(t){return this._prefix(t)+this.ttlKey},get:function(t){return this.isExpired(t)&&this.remove(t),r(s.getItem(this._prefix(t)))},set:function(t,r,o){return e.isNumber(o)?s.setItem(this._ttlKey(t),i(n()+o)):s.removeItem(this._ttlKey(t)),s.setItem(this._prefix(t),i(r))},remove:function(t){return s.removeItem(this._ttlKey(t)),s.removeItem(this._prefix(t)),this},clear:function(){var t,e,n=[],i=s.length;for(t=0;i>t;t++)(e=s.key(t)).match(this.keyMatcher)&&n.push(e.replace(this.keyMatcher,""));for(t=n.length;t--;)this.remove(n[t]);return this},isExpired:function(t){var i=r(s.getItem(this._ttlKey(t)));return e.isNumber(i)&&n()>i?!0:!1}}:{get:e.noop,set:e.noop,remove:e.noop,clear:e.noop,isExpired:e.noop},e.mixin(t.prototype,o),t}(),o=function(){"use strict";function n(e){e=e||{},this.cancelled=!1,this.lastUrl=null,this._send=e.transport?i(e.transport):t.ajax,this._get=e.rateLimiter?e.rateLimiter(this._get):this._get,this._cache=e.cache===!1?new r(0):a}function i(n){return function(i,r){function s(t){e.defer(function(){u.resolve(t)})}function o(t){e.defer(function(){u.reject(t)})}var u=t.Deferred();return n(i,r,s,o),u}}var s=0,o={},u=6,a=new r(10);return n.setMaxPendingRequests=function(t){u=t},n.resetCache=function(){a.reset()},e.mixin(n.prototype,{_get:function(t,e,n){function i(e){n&&n(null,e),h._cache.set(t,e)}function r(){n&&n(!0)}function a(){s--,delete o[t],h.onDeckRequestArgs&&(h._get.apply(h,h.onDeckRequestArgs),h.onDeckRequestArgs=null)}var c,h=this;this.cancelled||t!==this.lastUrl||((c=o[t])?c.done(i).fail(r):u>s?(s++,o[t]=this._send(t,e).done(i).fail(r).always(a)):this.onDeckRequestArgs=[].slice.call(arguments,0))},get:function(t,n,i){var r;return e.isFunction(n)&&(i=n,n={}),this.cancelled=!1,this.lastUrl=t,(r=this._cache.get(t))?e.defer(function(){i&&i(null,r)}):this._get(t,n,i),!!r},cancel:function(){this.cancelled=!0}}),n}(),u=function(){"use strict";function n(e){e=e||{},e.datumTokenizer&&e.queryTokenizer||t.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=e.datumTokenizer,this.queryTokenizer=e.queryTokenizer,this.reset()}function i(t){return t=e.filter(t,function(t){return!!t}),t=e.map(t,function(t){return t.toLowerCase()})}function r(){return{ids:[],children:{}}}function s(t){for(var e={},n=[],i=0,r=t.length;r>i;i++)e[t[i]]||(e[t[i]]=!0,n.push(t[i]));return n}function o(t,e){function n(t,e){return t-e}var i=0,r=0,s=[];t=t.sort(n),e=e.sort(n);for(var o=t.length,u=e.length;o>i&&u>r;)t[i]
e[r]?r++:(s.push(t[i]),i++,r++);return s}return e.mixin(n.prototype,{bootstrap:function(t){this.datums=t.datums,this.trie=t.trie},add:function(t){var n=this;t=e.isArray(t)?t:[t],e.each(t,function(t){var s,o;s=n.datums.push(t)-1,o=i(n.datumTokenizer(t)),e.each(o,function(t){var e,i,o;for(e=n.trie,i=t.split("");o=i.shift();)e=e.children[o]||(e.children[o]=r()),e.ids.push(s)})})},get:function(t){var n,r,u=this;return n=i(this.queryTokenizer(t)),e.each(n,function(t){var e,n,i,s;if(r&&0===r.length)return!1;for(e=u.trie,n=t.split("");e&&(i=n.shift());)e=e.children[i];return e&&0===n.length?(s=e.ids.slice(0),void(r=r?o(r,s):s)):(r=[],!1)}),r?e.map(s(r),function(t){return u.datums[t]}):[]},reset:function(){this.datums=[],this.trie=r()},serialize:function(){return{datums:this.datums,trie:this.trie}}}),n}(),a=function(){"use strict";function i(t){return t.local||null}function r(i){var r,s;return s={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(r=i.prefetch||null)&&(r=e.isString(r)?{url:r}:r,r=e.mixin(s,r),r.thumbprint=n+r.thumbprint,r.ajax.type=r.ajax.type||"GET",r.ajax.dataType=r.ajax.dataType||"json",!r.url&&t.error("prefetch requires url to be set")),r}function s(n){function i(t){return function(n){return e.debounce(n,t)}}function r(t){return function(n){return e.throttle(n,t)}}var s,o;return o={url:null,cache:!0,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(s=n.remote||null)&&(s=e.isString(s)?{url:s}:s,s=e.mixin(o,s),s.rateLimiter=/^throttle$/i.test(s.rateLimitBy)?r(s.rateLimitWait):i(s.rateLimitWait),s.ajax.type=s.ajax.type||"GET",s.ajax.dataType=s.ajax.dataType||"json",delete s.rateLimitBy,delete s.rateLimitWait,!s.url&&t.error("remote requires url to be set")),s}return{local:i,prefetch:r,remote:s}}();!function(n){"use strict";function r(e){e&&(e.local||e.prefetch||e.remote)||t.error("one of local, prefetch, or remote is required"),this.limit=e.limit||5,this.sorter=c(e.sorter),this.dupDetector=e.dupDetector||h,this.local=a.local(e),this.prefetch=a.prefetch(e),this.remote=a.remote(e),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new u({datumTokenizer:e.datumTokenizer,queryTokenizer:e.queryTokenizer}),this.storage=this.cacheKey?new s(this.cacheKey):null}function c(t){function n(e){return e.sort(t)}function i(t){return t}return e.isFunction(t)?n:i}function h(){return!1}var l,d;return l=n.Bloodhound,d={data:"data",protocol:"protocol",thumbprint:"thumbprint"},n.Bloodhound=r,r.noConflict=function(){return n.Bloodhound=l,r},r.tokenizers=i,e.mixin(r.prototype,{_loadPrefetch:function(e){function n(t){s.clear(),s.add(e.filter?e.filter(t):t),s._saveToStorage(s.index.serialize(),e.thumbprint,e.ttl)}var i,r,s=this;return(i=this._readFromStorage(e.thumbprint))?(this.index.bootstrap(i),r=t.Deferred().resolve()):r=t.ajax(e.url,e.ajax).done(n),r},_getFromRemote:function(t,e){function n(t,n){e(t?[]:s.remote.filter?s.remote.filter(n):n)}var i,r,s=this;if(this.transport)return t=t||"",r=encodeURIComponent(t),i=this.remote.replace?this.remote.replace(this.remote.url,t):this.remote.url.replace(this.remote.wildcard,r),this.transport.get(i,this.remote.ajax,n)},_cancelLastRemoteRequest:function(){this.transport&&this.transport.cancel()},_saveToStorage:function(t,e,n){this.storage&&(this.storage.set(d.data,t,n),this.storage.set(d.protocol,location.protocol,n),this.storage.set(d.thumbprint,e,n))},_readFromStorage:function(t){var e,n={};return this.storage&&(n.data=this.storage.get(d.data),n.protocol=this.storage.get(d.protocol),n.thumbprint=this.storage.get(d.thumbprint)),e=n.thumbprint!==t||n.protocol!==location.protocol,n.data&&!e?n.data:null},_initialize:function(){function n(){r.add(e.isFunction(s)?s():s)}var i,r=this,s=this.local;return i=this.prefetch?this._loadPrefetch(this.prefetch):t.Deferred().resolve(),s&&i.done(n),this.transport=this.remote?new o(this.remote):null,this.initPromise=i.promise()},initialize:function(t){return!this.initPromise||t?this._initialize():this.initPromise},add:function(t){this.index.add(t)},get:function(t,n){function i(t){var i=s.slice(0);e.each(t,function(t){var n;return n=e.some(i,function(e){return r.dupDetector(t,e)}),!n&&i.push(t),i.length0||!this.transport)&&n&&n(s)},clear:function(){this.index.reset()},clearPrefetchCache:function(){this.storage&&this.storage.clear()},clearRemoteCache:function(){this.transport&&o.resetCache()},ttAdapter:function(){return e.bind(this.get,this)}}),r}(this);var c=function(){return{wrapper:'',dropdown:'',dataset:'
',suggestions:' ',suggestion:'
'}}(),h=function(){"use strict";var t={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none",opacity:"1"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},suggestions:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};return e.isMsie()&&e.mixin(t.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),e.isMsie()&&e.isMsie()<=7&&e.mixin(t.input,{marginTop:"-1px"}),t}(),l=function(){"use strict";function n(e){e&&e.el||t.error("EventBus initialized without el"),this.$el=t(e.el)}var i="typeahead:";return e.mixin(n.prototype,{trigger:function(t){var e=[].slice.call(arguments,1);this.$el.trigger(i+t,e)}}),n}(),d=function(){"use strict";function t(t,e,n,i){var r;if(!n)return this;for(e=e.split(a),n=i?u(n,i):n,this._callbacks=this._callbacks||{};r=e.shift();)this._callbacks[r]=this._callbacks[r]||{sync:[],async:[]},this._callbacks[r][t].push(n);return this}function e(e,n,i){return t.call(this,"async",e,n,i)}function n(e,n,i){return t.call(this,"sync",e,n,i)}function i(t){var e;if(!this._callbacks)return this;for(t=t.split(a);e=t.shift();)delete this._callbacks[e];return this}function r(t){var e,n,i,r,o;if(!this._callbacks)return this;for(t=t.split(a),i=[].slice.call(arguments,1);(e=t.shift())&&(n=this._callbacks[e]);)r=s(n.sync,this,[e].concat(i)),o=s(n.async,this,[e].concat(i)),r()&&c(o);return this}function s(t,e,n){function i(){for(var i,r=0,s=t.length;!i&&s>r;r+=1)i=t[r].apply(e,n)===!1;return!i}return i}function o(){var t;return t=window.setImmediate?function(t){setImmediate(function(){t()})}:function(t){setTimeout(function(){t()},0)}}function u(t,e){return t.bind?t.bind(e):function(){t.apply(e,[].slice.call(arguments,0))}}var a=/\s+/,c=o();return{onSync:n,onAsync:e,off:i,trigger:r}}(),p=function(t){"use strict";function n(t,n,i){for(var r,s=[],o=0,u=t.length;u>o;o++)s.push(e.escapeRegExChars(t[o]));return r=i?"\\b("+s.join("|")+")\\b":"("+s.join("|")+")",n?new RegExp(r):new RegExp(r,"i")}var i={node:null,pattern:null,tagName:"strong",className:null,wordsOnly:!1,caseSensitive:!1};return function(r){function s(e){var n,i,s;return(n=u.exec(e.data))&&(s=t.createElement(r.tagName),r.className&&(s.className=r.className),i=e.splitText(n.index),i.splitText(n[0].length),s.appendChild(i.cloneNode(!0)),e.parentNode.replaceChild(s,i)),!!n}function o(t,e){for(var n,i=3,r=0;r').css({position:"absolute",visibility:"hidden",whiteSpace:"pre",fontFamily:e.css("font-family"),fontSize:e.css("font-size"),fontStyle:e.css("font-style"),fontVariant:e.css("font-variant"),fontWeight:e.css("font-weight"),wordSpacing:e.css("word-spacing"),letterSpacing:e.css("letter-spacing"),textIndent:e.css("text-indent"),textRendering:e.css("text-rendering"),textTransform:e.css("text-transform")}).insertAfter(e)}function r(t,e){return n.normalizeQuery(t)===n.normalizeQuery(e)}function s(t){return t.altKey||t.ctrlKey||t.metaKey||t.shiftKey}var o;return o={9:"tab",27:"esc",37:"left",39:"right",13:"enter",38:"up",40:"down"},n.normalizeQuery=function(t){return(t||"").replace(/^\s*/g,"").replace(/\s{2,}/g," ")},e.mixin(n.prototype,d,{_onBlur:function(){this.resetInputValue(),this.trigger("blurred")},_onFocus:function(){this.trigger("focused")},_onKeydown:function(t){var e=o[t.which||t.keyCode];this._managePreventDefault(e,t),e&&this._shouldTrigger(e,t)&&this.trigger(e+"Keyed",t)},_onInput:function(){this._checkInputValue()},_managePreventDefault:function(t,e){var n,i,r;switch(t){case"tab":i=this.getHint(),r=this.getInputValue(),n=i&&i!==r&&!s(e);break;case"up":case"down":n=!s(e);break;default:n=!1}n&&e.preventDefault()},_shouldTrigger:function(t,e){var n;switch(t){case"tab":n=!s(e);break;default:n=!0}return n},_checkInputValue:function(){var t,e,n;t=this.getInputValue(),e=r(t,this.query),n=e?this.query.length!==t.length:!1,this.query=t,e?n&&this.trigger("whitespaceChanged",this.query):this.trigger("queryChanged",this.query)},focus:function(){this.$input.focus()},blur:function(){this.$input.blur()},getQuery:function(){return this.query},setQuery:function(t){this.query=t},getInputValue:function(){return this.$input.val()},setInputValue:function(t,e){this.$input.val(t),e?this.clearHint():this._checkInputValue()},resetInputValue:function(){this.setInputValue(this.query,!0)},getHint:function(){return this.$hint.val()},setHint:function(t){this.$hint.val(t)},clearHint:function(){this.setHint("")},clearHintIfInvalid:function(){var t,e,n,i;t=this.getInputValue(),e=this.getHint(),n=t!==e&&0===e.indexOf(t),i=""!==t&&n&&!this.hasOverflow(),!i&&this.clearHint()},getLanguageDirection:function(){return(this.$input.css("direction")||"ltr").toLowerCase()},hasOverflow:function(){var t=this.$input.width()-2;return this.$overflowHelper.text(this.getInputValue()),this.$overflowHelper.width()>=t},isCursorAtEnd:function(){var t,n,i;return t=this.$input.val().length,n=this.$input[0].selectionStart,e.isNumber(n)?n===t:document.selection?(i=document.selection.createRange(),i.moveStart("character",-t),t===i.text.length):!0},destroy:function(){this.$hint.off(".tt"),this.$input.off(".tt"),this.$hint=this.$input=this.$overflowHelper=null}}),n}(),g=function(){"use strict";function n(n){n=n||{},n.templates=n.templates||{},n.source||t.error("missing source"),n.name&&!s(n.name)&&t.error("invalid dataset name: "+n.name),this.query=null,this.highlight=!!n.highlight,this.name=n.name||e.getUniqueId(),this.source=n.source,this.displayFn=i(n.display||n.displayKey),this.templates=r(n.templates,this.displayFn),this.$el=t(c.dataset.replace("%CLASS%",this.name))}function i(t){function n(e){return e[t]}return t=t||"value",e.isFunction(t)?t:n}function r(t,n){function i(t){return""+n(t)+"
"}return{empty:t.empty&&e.templatify(t.empty),header:t.header&&e.templatify(t.header),footer:t.footer&&e.templatify(t.footer),suggestion:t.suggestion||i}}function s(t){return/^[_a-zA-Z0-9-]+$/.test(t)}var o="ttDataset",u="ttValue",a="ttDatum";return n.extractDatasetName=function(e){return t(e).data(o)},n.extractValue=function(e){return t(e).data(u)},n.extractDatum=function(e){return t(e).data(a)},e.mixin(n.prototype,d,{_render:function(n,i){function r(){return g.templates.empty({query:n,isEmpty:!0})}function s(){function r(e){var n;return n=t(c.suggestion).append(g.templates.suggestion(e)).data(o,g.name).data(u,g.displayFn(e)).data(a,e),n.children().each(function(){t(this).css(h.suggestionChild)}),n}var s,l;return s=t(c.suggestions).css(h.suggestions),l=e.map(i,r),s.append.apply(s,l),g.highlight&&p({className:"tt-highlight",node:s[0],pattern:n}),s}function l(){return g.templates.header({query:n,isEmpty:!f})}function d(){return g.templates.footer({query:n,isEmpty:!f})}if(this.$el){var f,g=this;this.$el.empty(),f=i&&i.length,!f&&this.templates.empty?this.$el.html(r()).prepend(g.templates.header?l():null).append(g.templates.footer?d():null):f&&this.$el.html(s()).prepend(g.templates.header?l():null).append(g.templates.footer?d():null),this.trigger("rendered")}},getRoot:function(){return this.$el},update:function(t){function e(e){n.canceled||t!==n.query||n._render(t,e)}var n=this;this.query=t,this.canceled=!1,this.source(t,e)},cancel:function(){this.canceled=!0},clear:function(){this.cancel(),this.$el.empty(),this.trigger("rendered")},isEmpty:function(){return this.$el.is(":empty")},destroy:function(){this.$el=null}}),n}(),m=function(){"use strict";function n(n){var r,s,o,u=this;n=n||{},n.menu||t.error("menu is required"),this.isOpen=!1,this.isEmpty=!0,this.datasets=e.map(n.datasets,i),r=e.bind(this._onSuggestionClick,this),s=e.bind(this._onSuggestionMouseEnter,this),o=e.bind(this._onSuggestionMouseLeave,this),this.$menu=t(n.menu).on("click.tt",".tt-suggestion",r).on("mouseenter.tt",".tt-suggestion",s).on("mouseleave.tt",".tt-suggestion",o),e.each(this.datasets,function(t){u.$menu.append(t.getRoot()),t.onSync("rendered",u._onRendered,u)})}function i(t){return new g(t)}return e.mixin(n.prototype,d,{_onSuggestionClick:function(e){this.trigger("suggestionClicked",t(e.currentTarget))},_onSuggestionMouseEnter:function(e){this._removeCursor(),this._setCursor(t(e.currentTarget),!0)},_onSuggestionMouseLeave:function(){this._removeCursor()},_onRendered:function(){function t(t){return t.isEmpty()}this.isEmpty=e.every(this.datasets,t),this.isEmpty?this._hide():this.isOpen&&this._show(),this.trigger("datasetRendered")},_hide:function(){this.$menu.hide()},_show:function(){this.$menu.css("display","block")},_getSuggestions:function(){return this.$menu.find(".tt-suggestion")},_getCursor:function(){return this.$menu.find(".tt-cursor").first()},_setCursor:function(t,e){t.first().addClass("tt-cursor"),!e&&this.trigger("cursorMoved")},_removeCursor:function(){this._getCursor().removeClass("tt-cursor")},_moveCursor:function(t){var e,n,i,r;if(this.isOpen){if(n=this._getCursor(),e=this._getSuggestions(),this._removeCursor(),i=e.index(n)+t,i=(i+1)%(e.length+1)-1,-1===i)return void this.trigger("cursorRemoved");-1>i&&(i=e.length-1),this._setCursor(r=e.eq(i)),this._ensureVisible(r)}},_ensureVisible:function(t){var e,n,i,r;e=t.position().top,n=e+t.outerHeight(!0),i=this.$menu.scrollTop(),r=this.$menu.height()+parseInt(this.$menu.css("paddingTop"),10)+parseInt(this.$menu.css("paddingBottom"),10),0>e?this.$menu.scrollTop(i+e):n>r&&this.$menu.scrollTop(i+(n-r))},close:function(){this.isOpen&&(this.isOpen=!1,this._removeCursor(),this._hide(),this.trigger("closed"))},open:function(){this.isOpen||(this.isOpen=!0,!this.isEmpty&&this._show(),this.trigger("opened"))},setLanguageDirection:function(t){this.$menu.css("ltr"===t?h.ltr:h.rtl)},moveCursorUp:function(){this._moveCursor(-1)},moveCursorDown:function(){this._moveCursor(1)},getDatumForSuggestion:function(t){var e=null;return t.length&&(e={raw:g.extractDatum(t),value:g.extractValue(t),datasetName:g.extractDatasetName(t)}),e},getDatumForCursor:function(){return this.getDatumForSuggestion(this._getCursor().first())},getDatumForTopSuggestion:function(){return this.getDatumForSuggestion(this._getSuggestions().first())},update:function(t){function n(e){e.update(t)}e.each(this.datasets,n)},empty:function(){function t(t){t.clear()}e.each(this.datasets,t),this.isEmpty=!0},isVisible:function(){return this.isOpen&&!this.isEmpty},destroy:function(){function t(t){t.destroy()}this.$menu.off(".tt"),this.$menu=null,e.each(this.datasets,t)}}),n}(),y=function(){"use strict";function n(n){var r,s,o;n=n||{},n.input||t.error("missing input"),this.isActivated=!1,this.autoselect=!!n.autoselect,this.minLength=e.isNumber(n.minLength)?n.minLength:1,this.$node=i(n.input,n.withHint),r=this.$node.find(".tt-dropdown-menu"),s=this.$node.find(".tt-input"),o=this.$node.find(".tt-hint"),s.on("blur.tt",function(t){var n,i,o;n=document.activeElement,i=r.is(n),o=r.has(n).length>0,e.isMsie()&&(i||o)&&(t.preventDefault(),t.stopImmediatePropagation(),e.defer(function(){s.focus()}))}),r.on("mousedown.tt",function(t){t.preventDefault()}),this.eventBus=n.eventBus||new l({el:s}),this.dropdown=new m({menu:r,datasets:n.datasets}).onSync("suggestionClicked",this._onSuggestionClicked,this).onSync("cursorMoved",this._onCursorMoved,this).onSync("cursorRemoved",this._onCursorRemoved,this).onSync("opened",this._onOpened,this).onSync("closed",this._onClosed,this).onAsync("datasetRendered",this._onDatasetRendered,this),this.input=new f({input:s,hint:o}).onSync("focused",this._onFocused,this).onSync("blurred",this._onBlurred,this).onSync("enterKeyed",this._onEnterKeyed,this).onSync("tabKeyed",this._onTabKeyed,this).onSync("escKeyed",this._onEscKeyed,this).onSync("upKeyed",this._onUpKeyed,this).onSync("downKeyed",this._onDownKeyed,this).onSync("leftKeyed",this._onLeftKeyed,this).onSync("rightKeyed",this._onRightKeyed,this).onSync("queryChanged",this._onQueryChanged,this).onSync("whitespaceChanged",this._onWhitespaceChanged,this),this._setLanguageDirection()}function i(e,n){var i,s,u,a;i=t(e),s=t(c.wrapper).css(h.wrapper),u=t(c.dropdown).css(h.dropdown),a=i.clone().css(h.hint).css(r(i)),a.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder required").prop("readonly",!0).attr({autocomplete:"off",spellcheck:"false",tabindex:-1}),i.data(o,{dir:i.attr("dir"),autocomplete:i.attr("autocomplete"),spellcheck:i.attr("spellcheck"),style:i.attr("style")}),i.addClass("tt-input").attr({autocomplete:"off",spellcheck:!1}).css(n?h.input:h.inputWithNoHint);try{!i.attr("dir")&&i.attr("dir","auto")}catch(l){}return i.wrap(s).parent().prepend(n?a:null).append(u)}function r(t){return{backgroundAttachment:t.css("background-attachment"),backgroundClip:t.css("background-clip"),backgroundColor:t.css("background-color"),backgroundImage:t.css("background-image"),backgroundOrigin:t.css("background-origin"),backgroundPosition:t.css("background-position"),backgroundRepeat:t.css("background-repeat"),backgroundSize:t.css("background-size")}}function s(t){var n=t.find(".tt-input");e.each(n.data(o),function(t,i){e.isUndefined(t)?n.removeAttr(i):n.attr(i,t)}),n.detach().removeData(o).removeClass("tt-input").insertAfter(t),t.remove()}var o="ttAttrs";return e.mixin(n.prototype,{_onSuggestionClicked:function(t,e){var n;(n=this.dropdown.getDatumForSuggestion(e))&&this._select(n)},_onCursorMoved:function(){var t=this.dropdown.getDatumForCursor();this.input.setInputValue(t.value,!0),this.eventBus.trigger("cursorchanged",t.raw,t.datasetName)},_onCursorRemoved:function(){this.input.resetInputValue(),this._updateHint()},_onDatasetRendered:function(){this._updateHint()},_onOpened:function(){this._updateHint(),this.eventBus.trigger("opened")},_onClosed:function(){this.input.clearHint(),this.eventBus.trigger("closed")},_onFocused:function(){this.isActivated=!0,this.dropdown.open()},_onBlurred:function(){this.isActivated=!1,this.dropdown.empty(),this.dropdown.close()},_onEnterKeyed:function(t,e){var n,i;n=this.dropdown.getDatumForCursor(),i=this.dropdown.getDatumForTopSuggestion(),n?(this._select(n),e.preventDefault()):this.autoselect&&i&&(this._select(i),e.preventDefault())},_onTabKeyed:function(t,e){var n;(n=this.dropdown.getDatumForCursor())?(this._select(n),e.preventDefault()):this._autocomplete(!0)},_onEscKeyed:function(){this.dropdown.close(),this.input.resetInputValue()},_onUpKeyed:function(){var t=this.input.getQuery();this.dropdown.isEmpty&&t.length>=this.minLength?this.dropdown.update(t):this.dropdown.moveCursorUp(),this.dropdown.open()},_onDownKeyed:function(){var t=this.input.getQuery();this.dropdown.isEmpty&&t.length>=this.minLength?this.dropdown.update(t):this.dropdown.moveCursorDown(),this.dropdown.open()},_onLeftKeyed:function(){"rtl"===this.dir&&this._autocomplete()},_onRightKeyed:function(){"ltr"===this.dir&&this._autocomplete()},_onQueryChanged:function(t,e){this.input.clearHintIfInvalid(),e.length>=this.minLength?this.dropdown.update(e):this.dropdown.empty(),this.dropdown.open(),this._setLanguageDirection()},_onWhitespaceChanged:function(){this._updateHint(),this.dropdown.open()},_setLanguageDirection:function(){var t;this.dir!==(t=this.input.getLanguageDirection())&&(this.dir=t,this.$node.css("direction",t),this.dropdown.setLanguageDirection(t))},_updateHint:function(){var t,n,i,r,s,o;t=this.dropdown.getDatumForTopSuggestion(),t&&this.dropdown.isVisible()&&!this.input.hasOverflow()?(n=this.input.getInputValue(),i=f.normalizeQuery(n),r=e.escapeRegExChars(i),s=new RegExp("^(?:"+r+")(.+$)","i"),o=s.exec(t.value),o?this.input.setHint(n+o[1]):this.input.clearHint()):this.input.clearHint()},_autocomplete:function(t){var e,n,i,r;e=this.input.getHint(),n=this.input.getQuery(),i=t||this.input.isCursorAtEnd(),e&&n!==e&&i&&(r=this.dropdown.getDatumForTopSuggestion(),r&&this.input.setInputValue(r.value),this.eventBus.trigger("autocompleted",r.raw,r.datasetName))},_select:function(t){this.input.setQuery(t.value),this.input.setInputValue(t.value,!0),this._setLanguageDirection(),this.eventBus.trigger("selected",t.raw,t.datasetName),this.dropdown.close(),e.defer(e.bind(this.dropdown.empty,this.dropdown))},open:function(){this.dropdown.open()},close:function(){this.dropdown.close()},setVal:function(t){t=e.toStr(t),this.isActivated?this.input.setInputValue(t):(this.input.setQuery(t),this.input.setInputValue(t,!0)),this._setLanguageDirection()},getVal:function(){return this.input.getQuery()},destroy:function(){this.input.destroy(),this.dropdown.destroy(),s(this.$node),this.$node=null}}),n}();!function(){"use strict";var n,i,r;n=t.fn.typeahead,i="ttTypeahead",r={initialize:function(n,r){function s(){var s,o,u=t(this);e.each(r,function(t){t.highlight=!!n.highlight}),o=new y({input:u,eventBus:s=new l({el:u}),withHint:e.isUndefined(n.hint)?!0:!!n.hint,minLength:n.minLength,autoselect:n.autoselect,datasets:r}),u.data(i,o)}return r=e.isArray(r)?r:[].slice.call(arguments,1),n=n||{},this.each(s)},open:function(){function e(){var e,n=t(this);(e=n.data(i))&&e.open()}return this.each(e)},close:function(){function e(){var e,n=t(this);(e=n.data(i))&&e.close()}return this.each(e)},val:function(e){function n(){var n,r=t(this);(n=r.data(i))&&n.setVal(e)}function r(t){var e,n;return(e=t.data(i))&&(n=e.getVal()),n}return arguments.length?this.each(n):r(this.first())},destroy:function(){function e(){var e,n=t(this);(e=n.data(i))&&(e.destroy(),n.removeData(i))}return this.each(e)}},t.fn.typeahead=function(e){var n;return r[e]&&"initialize"!==e?(n=this.filter(function(){return!!t(this).data(i)}),r[e].apply(n,[].slice.call(arguments,1))):r.initialize.apply(this,arguments)},t.fn.typeahead.noConflict=function(){return t.fn.typeahead=n,this}}()}(window.jQuery);
diff --git a/docs/namespaces.html b/docs/namespaces.html
deleted file mode 100644
index 00cd118e..00000000
--- a/docs/namespaces.html
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
- Namespaces | API
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/opensearch.xml b/docs/opensearch.xml
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/renderer.index b/docs/renderer.index
deleted file mode 100644
index f402bc31..00000000
--- a/docs/renderer.index
+++ /dev/null
@@ -1 +0,0 @@
-C:19:"Sami\Renderer\Index":7184:{a:3:{i:0;a:81:{s:25:"API\Collection\Activities";s:40:"6cd0198ad0a9701b066ec7442f93db2f5f365c39";s:31:"API\Collection\ActivityProfiles";s:40:"f9ec3cf6e3ad876b68fd9245f9d00e4b01d647c0";s:29:"API\Collection\ActivityStates";s:40:"264dc40eb10fa94e49958e7bdf9d61a8a3de09e0";s:28:"API\Collection\AgentProfiles";s:40:"6a2da25c4acf0463bc3cb5bd8da3c39eba290255";s:26:"API\Collection\Attachments";s:40:"8683e48e070570eb345ca2957a9ab18be102283b";s:25:"API\Collection\AuthScopes";s:40:"804ec865e69c0c0d9b82379d65849679214924e2";s:26:"API\Collection\BasicTokens";s:40:"b37960aee8902fe2dda4a632bb02f53a51651102";s:27:"API\Collection\OAuthClients";s:40:"bb10233fb398947d63da80d245197b67ca33be09";s:26:"API\Collection\OAuthTokens";s:40:"6c9fb9af47933e576fd8dc43a135af06604494e1";s:25:"API\Collection\Statements";s:40:"b80bd3c35176ca673f8a9edfe5df4ce394b2f7b2";s:20:"API\Collection\Users";s:40:"e4c8907bda9abfc3e7b34563619c592584309fdd";s:11:"API\Command";s:40:"e9982d6ea83042c03eac69317ff66d779a216661";s:34:"API\Console\AuthScopeCreateCommand";s:40:"3b018a8f314747d0b2c5412f594f936c11f3276c";s:35:"API\Console\BasicTokenCreateCommand";s:40:"2eb2717ae5e03aafbe3c3e9d57923886b8878522";s:35:"API\Console\BasicTokenDeleteCommand";s:40:"282288cdc14599bd08fbca5c7eeff20a90b8a844";s:35:"API\Console\BasicTokenExpireCommand";s:40:"54d4b64277b216b648bfedc26541d1075b5b81b8";s:33:"API\Console\BasicTokenListCommand";s:40:"abf149928a09524dea4e0976f818fdcc11a2c6fd";s:36:"API\Console\OAuthClientCreateCommand";s:40:"cf20f43b7b8c67f5b7909211fae5bbeae8f9edd7";s:34:"API\Console\OAuthClientListCommand";s:40:"fb9d0e4e7f0cf402b1209f59c09d8eadab135ba8";s:26:"API\Console\SetupDbCommand";s:40:"84470058336ecec529e191c9ea28762b568cd2c5";s:29:"API\Console\SetupOAuthCommand";s:40:"6d0b35c69bb0076fd2dbf63250e72930660bda53";s:29:"API\Console\UserCreateCommand";s:40:"c821ae7c1d25e5369174dbf0b3225e8d377e1579";s:21:"API\Document\Activity";s:40:"dab0e526596bcd94722438eb3bb079ef7b070983";s:28:"API\Document\ActivityProfile";s:40:"e1fb69d4e1c852f2c78557c6e2c92cf258899a52";s:26:"API\Document\ActivityState";s:40:"9e4ae8ed67d135dcd3b440ce1074dd337b5e5224";s:25:"API\Document\AgentProfile";s:40:"07bf28f14f11ee72cdef1d87d9bc267732841fc9";s:23:"API\Document\Attachment";s:40:"55dd293651ac9a870d3a226f09bd95b525253301";s:31:"API\Document\Auth\AbstractToken";s:40:"3dbb2a2fc671cd0b01ae40fa1deba958accc1bce";s:28:"API\Document\Auth\BasicToken";s:40:"9620a1b68c2336b416a6150289dc1b1467af5c5c";s:29:"API\Document\Auth\OAuthClient";s:40:"3f7d3b8def46c3b569d117fdb7f2b70800f9544d";s:28:"API\Document\Auth\OAuthToken";s:40:"9e051f88eabba7ab81eaf9d85021405d8196af68";s:23:"API\Document\Auth\Scope";s:40:"1d855a7da5b5ec4f5939b07728709ef56e254c15";s:32:"API\Document\Auth\TokenInterface";s:40:"49dfa04210982c0bc173bc78d4dad75a3b83fd89";s:22:"API\Document\Statement";s:40:"c07ca3ed68aa2be271841c0e4ffbb1fa5413bc13";s:17:"API\Document\User";s:40:"2a08aec9248e8097ef5325b7629650fd640584f2";s:12:"API\Resource";s:40:"764b7af5641109ff561af57e17ef3dae00ed146a";s:22:"API\Resource\V10\About";s:40:"bbe66365b3d97fbcb9df0a4bbd1843b070ba9740";s:27:"API\Resource\V10\Activities";s:40:"86a573572761f1b1e711a2e54c40ae0e6355cbff";s:35:"API\Resource\V10\Activities\Profile";s:40:"699399d366a842ee5441060b9a96934b6e0a1298";s:33:"API\Resource\V10\Activities\State";s:40:"d676be2d8087fdabdc106810012b62bb5ea27036";s:23:"API\Resource\V10\Agents";s:40:"9caa770f16f38d4e287f15191e75f8ee43983ce1";s:31:"API\Resource\V10\Agents\Profile";s:40:"9cda9598cc04ed3227678040d6daa828d72eeebf";s:28:"API\Resource\V10\Attachments";s:40:"252bc91c7641d1e092b8e87cbbba8fecc33f14da";s:28:"API\Resource\V10\Auth\Tokens";s:40:"224f0cbfb2c11398bfe077803b846bd66868d121";s:32:"API\Resource\V10\Oauth\Authorize";s:40:"957a854b3540c680fbca6600400f9c581db3f367";s:28:"API\Resource\V10\Oauth\Login";s:40:"2fe3f6be93bde2d5fb042ba295edf86f98da87e7";s:28:"API\Resource\V10\Oauth\Token";s:40:"24baadba995e7d3cc2ebc597dd756a7badfacb0c";s:27:"API\Resource\V10\Statements";s:40:"2bdce3ec5486df0396ab6fc88d573b9acbbd133b";s:11:"API\Service";s:40:"191aa41590a02c2f0b3482ce8a464b793e52fcda";s:20:"API\Service\Activity";s:40:"d9b1ad00c980fa444fed8ac00ecac8734470d1b5";s:27:"API\Service\ActivityProfile";s:40:"7b77a10c546e526f1a866794cd98bcaa5ad00462";s:25:"API\Service\ActivityState";s:40:"81127a685c11b65583b27fb97cf1a60fd8537703";s:24:"API\Service\AgentProfile";s:40:"8b20523203afa44fe7a1e0695e234e1146d4756b";s:22:"API\Service\Attachment";s:40:"4f348da3c5e8a39c1cc6041711e4d0c4c549b5bc";s:30:"API\Service\Auth\AuthInterface";s:40:"39b362398e98eaf0588815c319d35a89fa07bcb3";s:22:"API\Service\Auth\Basic";s:40:"b4bd0afe37cd2779723c357df65e85316ca6275f";s:26:"API\Service\Auth\Exception";s:40:"fb680f2c346e541d921cbdbd6326f3bc00d27411";s:22:"API\Service\Auth\OAuth";s:40:"2c42d220df19bedab3d6516e44024df20cc36593";s:21:"API\Service\Exception";s:40:"56e1d24f3e0515ca06934ebbafdee4111482cf1b";s:21:"API\Service\Statement";s:40:"7c84f029b17c1261355e1667df9550196682d073";s:16:"API\Service\User";s:40:"71fd84aee0be300d01be2cedd6dab8c3389287f4";s:13:"API\Util\Date";s:40:"e2a032dbcd7a73b06de6527dfa38f90916c53910";s:19:"API\Util\Filesystem";s:40:"7b36638e152190bd0e40b92f73ce760ba9204af8";s:14:"API\Util\OAuth";s:40:"39202ffcabeae09fd4003de4dc3ff014c266ea68";s:13:"API\Validator";s:40:"e935526a735afa2243e76abf7fecb05711a94881";s:23:"API\Validator\Exception";s:40:"62ac6d557f1c0d9855a9a2958a4face45a498ef9";s:28:"API\Validator\V10\Attachment";s:40:"6760f0a850aa73e300a30c2b356a503ea800167b";s:27:"API\Validator\V10\Statement";s:40:"944ade9bae51130e115f02ed547b656e63a80d16";s:8:"API\View";s:40:"458f067c53c7a5140e288ff3d5bc42169100cbd4";s:18:"API\View\V10\About";s:40:"0a8a231cac46da57764ec76d0dde6d12f1cff4a1";s:21:"API\View\V10\Activity";s:40:"551b03a5ee96fcb8e6e45bbf09c032b21a3eee13";s:28:"API\View\V10\ActivityProfile";s:40:"235183c62058f33635bfe8ba651ab98fdd87f9dc";s:26:"API\View\V10\ActivityState";s:40:"e7391233be6c77cb6716a9e89735fc96952935dd";s:18:"API\View\V10\Agent";s:40:"692ea2f31d929896a5fca0cf833a466b936f9159";s:25:"API\View\V10\AgentProfile";s:40:"99b21e64b7dbf2b429d6f2ed80a6c48babdf51b9";s:25:"API\View\V10\BaseDocument";s:40:"f8a3bc5f88d248c9ffa171f8ffc4daf32bd5b5f5";s:34:"API\View\V10\BasicAuth\AccessToken";s:40:"633bd63e0fcc4e76b40eecd75e8fd94d1e27d579";s:30:"API\View\V10\OAuth\AccessToken";s:40:"5001455c87013c4b0aa320b4e118e370bca42075";s:28:"API\View\V10\OAuth\Authorize";s:40:"aa66ee226d672b768fbb68e62b5549da0b07d982";s:24:"API\View\V10\OAuth\Login";s:40:"a48831a069e96e11c6943dd8413257026ee96300";s:23:"API\View\V10\Statements";s:40:"f12ed874ea492f516aa08a7a21dc93b09d354fbb";}i:1;a:1:{i:0;s:6:"master";}i:2;a:20:{i:0;s:3:"API";i:1;s:14:"API\Collection";i:2;s:11:"API\Console";i:3;s:12:"API\Document";i:4;s:17:"API\Document\Auth";i:5;s:12:"API\Resource";i:6;s:16:"API\Resource\V10";i:7;s:27:"API\Resource\V10\Activities";i:8;s:23:"API\Resource\V10\Agents";i:9;s:21:"API\Resource\V10\Auth";i:10;s:22:"API\Resource\V10\Oauth";i:11;s:11:"API\Service";i:12;s:16:"API\Service\Auth";i:13;s:8:"API\Util";i:14;s:13:"API\Validator";i:15;s:17:"API\Validator\V10";i:16;s:8:"API\View";i:17;s:12:"API\View\V10";i:18;s:22:"API\View\V10\BasicAuth";i:19;s:18:"API\View\V10\OAuth";}}}
\ No newline at end of file
diff --git a/docs/sami.js b/docs/sami.js
deleted file mode 100644
index b56f3245..00000000
--- a/docs/sami.js
+++ /dev/null
@@ -1,685 +0,0 @@
-(function(root) {
-
- var bhIndex = null;
- var rootPath = '';
- var treeHtml = ' ';
-
- var searchTypeClasses = {
- 'Namespace': 'label-default',
- 'Class': 'label-info',
- 'Interface': 'label-primary',
- 'Trait': 'label-success',
- 'Method': 'label-danger',
- '_': 'label-warning'
- };
-
- var searchIndex = [
- {"type": "Namespace", "link": "API.html", "name": "API", "doc": "Namespace API"},{"type": "Namespace", "link": "API/Collection.html", "name": "API\\Collection", "doc": "Namespace API\\Collection"},{"type": "Namespace", "link": "API/Console.html", "name": "API\\Console", "doc": "Namespace API\\Console"},{"type": "Namespace", "link": "API/Document.html", "name": "API\\Document", "doc": "Namespace API\\Document"},{"type": "Namespace", "link": "API/Document/Auth.html", "name": "API\\Document\\Auth", "doc": "Namespace API\\Document\\Auth"},{"type": "Namespace", "link": "API/Resource.html", "name": "API\\Resource", "doc": "Namespace API\\Resource"},{"type": "Namespace", "link": "API/Resource/V10.html", "name": "API\\Resource\\V10", "doc": "Namespace API\\Resource\\V10"},{"type": "Namespace", "link": "API/Resource/V10/Activities.html", "name": "API\\Resource\\V10\\Activities", "doc": "Namespace API\\Resource\\V10\\Activities"},{"type": "Namespace", "link": "API/Resource/V10/Agents.html", "name": "API\\Resource\\V10\\Agents", "doc": "Namespace API\\Resource\\V10\\Agents"},{"type": "Namespace", "link": "API/Resource/V10/Auth.html", "name": "API\\Resource\\V10\\Auth", "doc": "Namespace API\\Resource\\V10\\Auth"},{"type": "Namespace", "link": "API/Resource/V10/Oauth.html", "name": "API\\Resource\\V10\\Oauth", "doc": "Namespace API\\Resource\\V10\\Oauth"},{"type": "Namespace", "link": "API/Service.html", "name": "API\\Service", "doc": "Namespace API\\Service"},{"type": "Namespace", "link": "API/Service/Auth.html", "name": "API\\Service\\Auth", "doc": "Namespace API\\Service\\Auth"},{"type": "Namespace", "link": "API/Util.html", "name": "API\\Util", "doc": "Namespace API\\Util"},{"type": "Namespace", "link": "API/Validator.html", "name": "API\\Validator", "doc": "Namespace API\\Validator"},{"type": "Namespace", "link": "API/Validator/V10.html", "name": "API\\Validator\\V10", "doc": "Namespace API\\Validator\\V10"},{"type": "Namespace", "link": "API/View.html", "name": "API\\View", "doc": "Namespace API\\View"},{"type": "Namespace", "link": "API/View/V10.html", "name": "API\\View\\V10", "doc": "Namespace API\\View\\V10"},{"type": "Namespace", "link": "API/View/V10/BasicAuth.html", "name": "API\\View\\V10\\BasicAuth", "doc": "Namespace API\\View\\V10\\BasicAuth"},{"type": "Namespace", "link": "API/View/V10/OAuth.html", "name": "API\\View\\V10\\OAuth", "doc": "Namespace API\\View\\V10\\OAuth"},
- {"type": "Interface", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/TokenInterface.html", "name": "API\\Document\\Auth\\TokenInterface", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_hasPermission", "name": "API\\Document\\Auth\\TokenInterface::hasPermission", "doc": ""Does the user have a certain permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_checkPermission", "name": "API\\Document\\Auth\\TokenInterface::checkPermission", "doc": ""Throws an exception if the user doesn't possess the given permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_isValid", "name": "API\\Document\\Auth\\TokenInterface::isValid", "doc": ""Is this user valid? I.e. expired token etc.""},
-
- {"type": "Interface", "fromName": "API\\Service\\Auth", "fromLink": "API/Service/Auth.html", "link": "API/Service/Auth/AuthInterface.html", "name": "API\\Service\\Auth\\AuthInterface", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\AuthInterface", "fromLink": "API/Service/Auth/AuthInterface.html", "link": "API/Service/Auth/AuthInterface.html#method_extractToken", "name": "API\\Service\\Auth\\AuthInterface::extractToken", "doc": ""Fetches the token document, parsing it from the request.""},
-
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/Activities.html", "name": "API\\Collection\\Activities", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\Activities", "fromLink": "API/Collection/Activities.html", "link": "API/Collection/Activities.html#method_getDocumentClassName", "name": "API\\Collection\\Activities::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/ActivityProfiles.html", "name": "API\\Collection\\ActivityProfiles", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\ActivityProfiles", "fromLink": "API/Collection/ActivityProfiles.html", "link": "API/Collection/ActivityProfiles.html#method_getDocumentClassName", "name": "API\\Collection\\ActivityProfiles::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/ActivityStates.html", "name": "API\\Collection\\ActivityStates", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\ActivityStates", "fromLink": "API/Collection/ActivityStates.html", "link": "API/Collection/ActivityStates.html#method_getDocumentClassName", "name": "API\\Collection\\ActivityStates::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/AgentProfiles.html", "name": "API\\Collection\\AgentProfiles", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\AgentProfiles", "fromLink": "API/Collection/AgentProfiles.html", "link": "API/Collection/AgentProfiles.html#method_getDocumentClassName", "name": "API\\Collection\\AgentProfiles::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/Attachments.html", "name": "API\\Collection\\Attachments", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\Attachments", "fromLink": "API/Collection/Attachments.html", "link": "API/Collection/Attachments.html#method_getDocumentClassName", "name": "API\\Collection\\Attachments::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/AuthScopes.html", "name": "API\\Collection\\AuthScopes", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\AuthScopes", "fromLink": "API/Collection/AuthScopes.html", "link": "API/Collection/AuthScopes.html#method_getDocumentClassName", "name": "API\\Collection\\AuthScopes::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/BasicTokens.html", "name": "API\\Collection\\BasicTokens", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\BasicTokens", "fromLink": "API/Collection/BasicTokens.html", "link": "API/Collection/BasicTokens.html#method_getDocumentClassName", "name": "API\\Collection\\BasicTokens::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/OAuthClients.html", "name": "API\\Collection\\OAuthClients", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\OAuthClients", "fromLink": "API/Collection/OAuthClients.html", "link": "API/Collection/OAuthClients.html#method_getDocumentClassName", "name": "API\\Collection\\OAuthClients::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/OAuthTokens.html", "name": "API\\Collection\\OAuthTokens", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\OAuthTokens", "fromLink": "API/Collection/OAuthTokens.html", "link": "API/Collection/OAuthTokens.html#method_getDocumentClassName", "name": "API\\Collection\\OAuthTokens::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/Statements.html", "name": "API\\Collection\\Statements", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\Statements", "fromLink": "API/Collection/Statements.html", "link": "API/Collection/Statements.html#method_getDocumentClassName", "name": "API\\Collection\\Statements::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Collection", "fromLink": "API/Collection.html", "link": "API/Collection/Users.html", "name": "API\\Collection\\Users", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Collection\\Users", "fromLink": "API/Collection/Users.html", "link": "API/Collection/Users.html#method_getDocumentClassName", "name": "API\\Collection\\Users::getDocumentClassName", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API", "fromLink": "API.html", "link": "API/Command.html", "name": "API\\Command", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Command", "fromLink": "API/Command.html", "link": "API/Command.html#method___construct", "name": "API\\Command::__construct", "doc": ""Construct.""},
- {"type": "Method", "fromName": "API\\Command", "fromLink": "API/Command.html", "link": "API/Command.html#method_init", "name": "API\\Command::init", "doc": ""Default init, use for overwrite only.""},
- {"type": "Method", "fromName": "API\\Command", "fromLink": "API/Command.html", "link": "API/Command.html#method_getSlim", "name": "API\\Command::getSlim", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Command", "fromLink": "API/Command.html", "link": "API/Command.html#method_setSlim", "name": "API\\Command::setSlim", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/AuthScopeCreateCommand.html", "name": "API\\Console\\AuthScopeCreateCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/BasicTokenCreateCommand.html", "name": "API\\Console\\BasicTokenCreateCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/BasicTokenDeleteCommand.html", "name": "API\\Console\\BasicTokenDeleteCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/BasicTokenExpireCommand.html", "name": "API\\Console\\BasicTokenExpireCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/BasicTokenListCommand.html", "name": "API\\Console\\BasicTokenListCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/OAuthClientCreateCommand.html", "name": "API\\Console\\OAuthClientCreateCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/OAuthClientListCommand.html", "name": "API\\Console\\OAuthClientListCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/SetupDbCommand.html", "name": "API\\Console\\SetupDbCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/SetupOAuthCommand.html", "name": "API\\Console\\SetupOAuthCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Console", "fromLink": "API/Console.html", "link": "API/Console/UserCreateCommand.html", "name": "API\\Console\\UserCreateCommand", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/Activity.html", "name": "API\\Document\\Activity", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/ActivityProfile.html", "name": "API\\Document\\ActivityProfile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\ActivityProfile", "fromLink": "API/Document/ActivityProfile.html", "link": "API/Document/ActivityProfile.html#method_getIdentifier", "name": "API\\Document\\ActivityProfile::getIdentifier", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/ActivityState.html", "name": "API\\Document\\ActivityState", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\ActivityState", "fromLink": "API/Document/ActivityState.html", "link": "API/Document/ActivityState.html#method_getIdentifier", "name": "API\\Document\\ActivityState::getIdentifier", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/AgentProfile.html", "name": "API\\Document\\AgentProfile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\AgentProfile", "fromLink": "API/Document/AgentProfile.html", "link": "API/Document/AgentProfile.html#method_getIdentifier", "name": "API\\Document\\AgentProfile::getIdentifier", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/Attachment.html", "name": "API\\Document\\Attachment", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_setSha2", "name": "API\\Document\\Attachment::setSha2", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_getSha2", "name": "API\\Document\\Attachment::getSha2", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_setContentType", "name": "API\\Document\\Attachment::setContentType", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_getContentType", "name": "API\\Document\\Attachment::getContentType", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_setTimestamp", "name": "API\\Document\\Attachment::setTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Attachment", "fromLink": "API/Document/Attachment.html", "link": "API/Document/Attachment.html#method_getTimestamp", "name": "API\\Document\\Attachment::getTimestamp", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/AbstractToken.html", "name": "API\\Document\\Auth\\AbstractToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_addScope", "name": "API\\Document\\Auth\\AbstractToken::addScope", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_isSuperToken", "name": "API\\Document\\Auth\\AbstractToken::isSuperToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_hasPermission", "name": "API\\Document\\Auth\\AbstractToken::hasPermission", "doc": ""Does the user have a certain permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_checkPermission", "name": "API\\Document\\Auth\\AbstractToken::checkPermission", "doc": ""Throws an exception if the user doesn't possess the given permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_getExpiresIn", "name": "API\\Document\\Auth\\AbstractToken::getExpiresIn", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_setExpiresIn", "name": "API\\Document\\Auth\\AbstractToken::setExpiresIn", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_isExpired", "name": "API\\Document\\Auth\\AbstractToken::isExpired", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_isValid", "name": "API\\Document\\Auth\\AbstractToken::isValid", "doc": ""Is this user valid? I.e. expired token etc.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\AbstractToken", "fromLink": "API/Document/Auth/AbstractToken.html", "link": "API/Document/Auth/AbstractToken.html#method_jsonSerialize", "name": "API\\Document\\Auth\\AbstractToken::jsonSerialize", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/BasicToken.html", "name": "API\\Document\\Auth\\BasicToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\BasicToken", "fromLink": "API/Document/Auth/BasicToken.html", "link": "API/Document/Auth/BasicToken.html#method_relations", "name": "API\\Document\\Auth\\BasicToken::relations", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\BasicToken", "fromLink": "API/Document/Auth/BasicToken.html", "link": "API/Document/Auth/BasicToken.html#method_generateAuthority", "name": "API\\Document\\Auth\\BasicToken::generateAuthority", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/OAuthClient.html", "name": "API\\Document\\Auth\\OAuthClient", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\OAuthClient", "fromLink": "API/Document/Auth/OAuthClient.html", "link": "API/Document/Auth/OAuthClient.html#method_relations", "name": "API\\Document\\Auth\\OAuthClient::relations", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\OAuthClient", "fromLink": "API/Document/Auth/OAuthClient.html", "link": "API/Document/Auth/OAuthClient.html#method_jsonSerialize", "name": "API\\Document\\Auth\\OAuthClient::jsonSerialize", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/OAuthToken.html", "name": "API\\Document\\Auth\\OAuthToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\OAuthToken", "fromLink": "API/Document/Auth/OAuthToken.html", "link": "API/Document/Auth/OAuthToken.html#method_relations", "name": "API\\Document\\Auth\\OAuthToken::relations", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\OAuthToken", "fromLink": "API/Document/Auth/OAuthToken.html", "link": "API/Document/Auth/OAuthToken.html#method_getExpired", "name": "API\\Document\\Auth\\OAuthToken::getExpired", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\OAuthToken", "fromLink": "API/Document/Auth/OAuthToken.html", "link": "API/Document/Auth/OAuthToken.html#method_generateAuthority", "name": "API\\Document\\Auth\\OAuthToken::generateAuthority", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/Scope.html", "name": "API\\Document\\Auth\\Scope", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\Scope", "fromLink": "API/Document/Auth/Scope.html", "link": "API/Document/Auth/Scope.html#method_relations", "name": "API\\Document\\Auth\\Scope::relations", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\Scope", "fromLink": "API/Document/Auth/Scope.html", "link": "API/Document/Auth/Scope.html#method_jsonSerialize", "name": "API\\Document\\Auth\\Scope::jsonSerialize", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document\\Auth", "fromLink": "API/Document/Auth.html", "link": "API/Document/Auth/TokenInterface.html", "name": "API\\Document\\Auth\\TokenInterface", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_hasPermission", "name": "API\\Document\\Auth\\TokenInterface::hasPermission", "doc": ""Does the user have a certain permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_checkPermission", "name": "API\\Document\\Auth\\TokenInterface::checkPermission", "doc": ""Throws an exception if the user doesn't possess the given permission.""},
- {"type": "Method", "fromName": "API\\Document\\Auth\\TokenInterface", "fromLink": "API/Document/Auth/TokenInterface.html", "link": "API/Document/Auth/TokenInterface.html#method_isValid", "name": "API\\Document\\Auth\\TokenInterface::isValid", "doc": ""Is this user valid? I.e. expired token etc.""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/Statement.html", "name": "API\\Document\\Statement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setStatement", "name": "API\\Document\\Statement::setStatement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_getStatement", "name": "API\\Document\\Statement::getStatement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setStored", "name": "API\\Document\\Statement::setStored", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_getStored", "name": "API\\Document\\Statement::getStored", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setTimestamp", "name": "API\\Document\\Statement::setTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_getTimestamp", "name": "API\\Document\\Statement::getTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setMongoTimestamp", "name": "API\\Document\\Statement::setMongoTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_getMongoTimestamp", "name": "API\\Document\\Statement::getMongoTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setDefaultTimestamp", "name": "API\\Document\\Statement::setDefaultTimestamp", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_isVoiding", "name": "API\\Document\\Statement::isVoiding", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_getReferencedStatement", "name": "API\\Document\\Statement::getReferencedStatement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_fixAttachmentLinks", "name": "API\\Document\\Statement::fixAttachmentLinks", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_extractActivities", "name": "API\\Document\\Statement::extractActivities", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_jsonSerialize", "name": "API\\Document\\Statement::jsonSerialize", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_setDefaultId", "name": "API\\Document\\Statement::setDefaultId", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_renderExact", "name": "API\\Document\\Statement::renderExact", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\Statement", "fromLink": "API/Document/Statement.html", "link": "API/Document/Statement.html#method_renderMeta", "name": "API\\Document\\Statement::renderMeta", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Document", "fromLink": "API/Document.html", "link": "API/Document/User.html", "name": "API\\Document\\User", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_relations", "name": "API\\Document\\User::relations", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_addPermission", "name": "API\\Document\\User::addPermission", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_isSuperUser", "name": "API\\Document\\User::isSuperUser", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_hasPermission", "name": "API\\Document\\User::hasPermission", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_checkPermission", "name": "API\\Document\\User::checkPermission", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_getExpiresIn", "name": "API\\Document\\User::getExpiresIn", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_setExpiresIn", "name": "API\\Document\\User::setExpiresIn", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_getExpired", "name": "API\\Document\\User::getExpired", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_getValid", "name": "API\\Document\\User::getValid", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_renderSummary", "name": "API\\Document\\User::renderSummary", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Document\\User", "fromLink": "API/Document/User.html", "link": "API/Document/User.html#method_jsonSerialize", "name": "API\\Document\\User::jsonSerialize", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API", "fromLink": "API.html", "link": "API/Resource.html", "name": "API\\Resource", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method___construct", "name": "API\\Resource::__construct", "doc": ""Construct.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_init", "name": "API\\Resource::init", "doc": ""Default init, use for overwrite only.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_get", "name": "API\\Resource::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_post", "name": "API\\Resource::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_put", "name": "API\\Resource::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_delete", "name": "API\\Resource::delete", "doc": ""Default delete method.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_options", "name": "API\\Resource::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_error", "name": "API\\Resource::error", "doc": ""Error handler.""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_response", "name": "API\\Resource::response", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_jsonResponse", "name": "API\\Resource::jsonResponse", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_multipartResponse", "name": "API\\Resource::multipartResponse", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_load", "name": "API\\Resource::load", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_getSlim", "name": "API\\Resource::getSlim", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_setSlim", "name": "API\\Resource::setSlim", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource", "fromLink": "API/Resource.html", "link": "API/Resource.html#method_getDocumentManager", "name": "API\\Resource::getDocumentManager", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10", "fromLink": "API/Resource/V10.html", "link": "API/Resource/V10/About.html", "name": "API\\Resource\\V10\\About", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\About", "fromLink": "API/Resource/V10/About.html", "link": "API/Resource/V10/About.html#method_get", "name": "API\\Resource\\V10\\About::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\About", "fromLink": "API/Resource/V10/About.html", "link": "API/Resource/V10/About.html#method_options", "name": "API\\Resource\\V10\\About::options", "doc": ""General options method.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10", "fromLink": "API/Resource/V10.html", "link": "API/Resource/V10/Activities.html", "name": "API\\Resource\\V10\\Activities", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities.html#method_init", "name": "API\\Resource\\V10\\Activities::init", "doc": ""Get activity service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities.html#method_get", "name": "API\\Resource\\V10\\Activities::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities.html#method_options", "name": "API\\Resource\\V10\\Activities::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities.html#method_getActivityService", "name": "API\\Resource\\V10\\Activities::getActivityService", "doc": ""Gets the value of activityService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities.html#method_setActivityService", "name": "API\\Resource\\V10\\Activities::setActivityService", "doc": ""Sets the value of activityService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities/Profile.html", "name": "API\\Resource\\V10\\Activities\\Profile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_init", "name": "API\\Resource\\V10\\Activities\\Profile::init", "doc": ""Get activity service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_get", "name": "API\\Resource\\V10\\Activities\\Profile::get", "doc": ""Handle the Statement GET request.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_put", "name": "API\\Resource\\V10\\Activities\\Profile::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_post", "name": "API\\Resource\\V10\\Activities\\Profile::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_delete", "name": "API\\Resource\\V10\\Activities\\Profile::delete", "doc": ""Default delete method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_options", "name": "API\\Resource\\V10\\Activities\\Profile::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_getActivityProfileService", "name": "API\\Resource\\V10\\Activities\\Profile::getActivityProfileService", "doc": ""Gets the value of activityProfileService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\Profile", "fromLink": "API/Resource/V10/Activities/Profile.html", "link": "API/Resource/V10/Activities/Profile.html#method_setActivityProfileService", "name": "API\\Resource\\V10\\Activities\\Profile::setActivityProfileService", "doc": ""Sets the value of activityProfileService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Activities", "fromLink": "API/Resource/V10/Activities.html", "link": "API/Resource/V10/Activities/State.html", "name": "API\\Resource\\V10\\Activities\\State", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_init", "name": "API\\Resource\\V10\\Activities\\State::init", "doc": ""Get activity service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_get", "name": "API\\Resource\\V10\\Activities\\State::get", "doc": ""Handle the Statement GET request.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_put", "name": "API\\Resource\\V10\\Activities\\State::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_post", "name": "API\\Resource\\V10\\Activities\\State::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_delete", "name": "API\\Resource\\V10\\Activities\\State::delete", "doc": ""Default delete method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_options", "name": "API\\Resource\\V10\\Activities\\State::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_getActivityStateService", "name": "API\\Resource\\V10\\Activities\\State::getActivityStateService", "doc": ""Gets the value of activityStateService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Activities\\State", "fromLink": "API/Resource/V10/Activities/State.html", "link": "API/Resource/V10/Activities/State.html#method_setActivityStateService", "name": "API\\Resource\\V10\\Activities\\State::setActivityStateService", "doc": ""Sets the value of activityStateService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10", "fromLink": "API/Resource/V10.html", "link": "API/Resource/V10/Agents.html", "name": "API\\Resource\\V10\\Agents", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents", "fromLink": "API/Resource/V10/Agents.html", "link": "API/Resource/V10/Agents.html#method_get", "name": "API\\Resource\\V10\\Agents::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents", "fromLink": "API/Resource/V10/Agents.html", "link": "API/Resource/V10/Agents.html#method_options", "name": "API\\Resource\\V10\\Agents::options", "doc": ""General options method.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Agents", "fromLink": "API/Resource/V10/Agents.html", "link": "API/Resource/V10/Agents/Profile.html", "name": "API\\Resource\\V10\\Agents\\Profile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_init", "name": "API\\Resource\\V10\\Agents\\Profile::init", "doc": ""Get agent profile service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_get", "name": "API\\Resource\\V10\\Agents\\Profile::get", "doc": ""Handle the Statement GET request.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_put", "name": "API\\Resource\\V10\\Agents\\Profile::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_post", "name": "API\\Resource\\V10\\Agents\\Profile::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_delete", "name": "API\\Resource\\V10\\Agents\\Profile::delete", "doc": ""Default delete method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_options", "name": "API\\Resource\\V10\\Agents\\Profile::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_getAgentProfileService", "name": "API\\Resource\\V10\\Agents\\Profile::getAgentProfileService", "doc": ""Gets the value of agentProfileService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Agents\\Profile", "fromLink": "API/Resource/V10/Agents/Profile.html", "link": "API/Resource/V10/Agents/Profile.html#method_setAgentProfileService", "name": "API\\Resource\\V10\\Agents\\Profile::setAgentProfileService", "doc": ""Sets the value of agentProfileService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10", "fromLink": "API/Resource/V10.html", "link": "API/Resource/V10/Attachments.html", "name": "API\\Resource\\V10\\Attachments", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Attachments", "fromLink": "API/Resource/V10/Attachments.html", "link": "API/Resource/V10/Attachments.html#method_init", "name": "API\\Resource\\V10\\Attachments::init", "doc": ""Get statement service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Attachments", "fromLink": "API/Resource/V10/Attachments.html", "link": "API/Resource/V10/Attachments.html#method_get", "name": "API\\Resource\\V10\\Attachments::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Attachments", "fromLink": "API/Resource/V10/Attachments.html", "link": "API/Resource/V10/Attachments.html#method_options", "name": "API\\Resource\\V10\\Attachments::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Attachments", "fromLink": "API/Resource/V10/Attachments.html", "link": "API/Resource/V10/Attachments.html#method_getAttachmentService", "name": "API\\Resource\\V10\\Attachments::getAttachmentService", "doc": ""Gets the value of attachmentService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Attachments", "fromLink": "API/Resource/V10/Attachments.html", "link": "API/Resource/V10/Attachments.html#method_setAttachmentService", "name": "API\\Resource\\V10\\Attachments::setAttachmentService", "doc": ""Sets the value of attachmentService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Auth", "fromLink": "API/Resource/V10/Auth.html", "link": "API/Resource/V10/Auth/Tokens.html", "name": "API\\Resource\\V10\\Auth\\Tokens", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_init", "name": "API\\Resource\\V10\\Auth\\Tokens::init", "doc": ""Get agent profile service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_get", "name": "API\\Resource\\V10\\Auth\\Tokens::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_post", "name": "API\\Resource\\V10\\Auth\\Tokens::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_put", "name": "API\\Resource\\V10\\Auth\\Tokens::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_delete", "name": "API\\Resource\\V10\\Auth\\Tokens::delete", "doc": ""Default delete method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_options", "name": "API\\Resource\\V10\\Auth\\Tokens::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_getAccessTokenService", "name": "API\\Resource\\V10\\Auth\\Tokens::getAccessTokenService", "doc": ""Gets the value of accessTokenService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Auth\\Tokens", "fromLink": "API/Resource/V10/Auth/Tokens.html", "link": "API/Resource/V10/Auth/Tokens.html#method_setAccessTokenService", "name": "API\\Resource\\V10\\Auth\\Tokens::setAccessTokenService", "doc": ""Sets the value of accessTokenService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Oauth", "fromLink": "API/Resource/V10/Oauth.html", "link": "API/Resource/V10/Oauth/Authorize.html", "name": "API\\Resource\\V10\\Oauth\\Authorize", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_init", "name": "API\\Resource\\V10\\Oauth\\Authorize::init", "doc": ""Get agent profile service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_get", "name": "API\\Resource\\V10\\Oauth\\Authorize::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_post", "name": "API\\Resource\\V10\\Oauth\\Authorize::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_options", "name": "API\\Resource\\V10\\Oauth\\Authorize::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_getOAuthService", "name": "API\\Resource\\V10\\Oauth\\Authorize::getOAuthService", "doc": ""Gets the value of oAuthService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_setOAuthService", "name": "API\\Resource\\V10\\Oauth\\Authorize::setOAuthService", "doc": ""Sets the value of oAuthService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_getUserService", "name": "API\\Resource\\V10\\Oauth\\Authorize::getUserService", "doc": ""Gets the value of userService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Authorize", "fromLink": "API/Resource/V10/Oauth/Authorize.html", "link": "API/Resource/V10/Oauth/Authorize.html#method_setUserService", "name": "API\\Resource\\V10\\Oauth\\Authorize::setUserService", "doc": ""Sets the value of userService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Oauth", "fromLink": "API/Resource/V10/Oauth.html", "link": "API/Resource/V10/Oauth/Login.html", "name": "API\\Resource\\V10\\Oauth\\Login", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_init", "name": "API\\Resource\\V10\\Oauth\\Login::init", "doc": ""Get agent profile service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_get", "name": "API\\Resource\\V10\\Oauth\\Login::get", "doc": ""Default get method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_post", "name": "API\\Resource\\V10\\Oauth\\Login::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_options", "name": "API\\Resource\\V10\\Oauth\\Login::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_getOAuthService", "name": "API\\Resource\\V10\\Oauth\\Login::getOAuthService", "doc": ""Gets the value of oAuthService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_setOAuthService", "name": "API\\Resource\\V10\\Oauth\\Login::setOAuthService", "doc": ""Sets the value of oAuthService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_getUserService", "name": "API\\Resource\\V10\\Oauth\\Login::getUserService", "doc": ""Gets the value of userService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Login", "fromLink": "API/Resource/V10/Oauth/Login.html", "link": "API/Resource/V10/Oauth/Login.html#method_setUserService", "name": "API\\Resource\\V10\\Oauth\\Login::setUserService", "doc": ""Sets the value of userService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10\\Oauth", "fromLink": "API/Resource/V10/Oauth.html", "link": "API/Resource/V10/Oauth/Token.html", "name": "API\\Resource\\V10\\Oauth\\Token", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Token", "fromLink": "API/Resource/V10/Oauth/Token.html", "link": "API/Resource/V10/Oauth/Token.html#method_init", "name": "API\\Resource\\V10\\Oauth\\Token::init", "doc": ""Get agent profile service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Token", "fromLink": "API/Resource/V10/Oauth/Token.html", "link": "API/Resource/V10/Oauth/Token.html#method_post", "name": "API\\Resource\\V10\\Oauth\\Token::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Token", "fromLink": "API/Resource/V10/Oauth/Token.html", "link": "API/Resource/V10/Oauth/Token.html#method_options", "name": "API\\Resource\\V10\\Oauth\\Token::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Token", "fromLink": "API/Resource/V10/Oauth/Token.html", "link": "API/Resource/V10/Oauth/Token.html#method_getOAuthService", "name": "API\\Resource\\V10\\Oauth\\Token::getOAuthService", "doc": ""Gets the value of oAuthService.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Oauth\\Token", "fromLink": "API/Resource/V10/Oauth/Token.html", "link": "API/Resource/V10/Oauth/Token.html#method_setOAuthService", "name": "API\\Resource\\V10\\Oauth\\Token::setOAuthService", "doc": ""Sets the value of oAuthService.""},
-
- {"type": "Class", "fromName": "API\\Resource\\V10", "fromLink": "API/Resource/V10.html", "link": "API/Resource/V10/Statements.html", "name": "API\\Resource\\V10\\Statements", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_init", "name": "API\\Resource\\V10\\Statements::init", "doc": ""Get statement service.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_get", "name": "API\\Resource\\V10\\Statements::get", "doc": ""Handle the Statement GET request.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_put", "name": "API\\Resource\\V10\\Statements::put", "doc": ""Default put method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_post", "name": "API\\Resource\\V10\\Statements::post", "doc": ""Default post method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_options", "name": "API\\Resource\\V10\\Statements::options", "doc": ""General options method.""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_getStatementService", "name": "API\\Resource\\V10\\Statements::getStatementService", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_setStatementService", "name": "API\\Resource\\V10\\Statements::setStatementService", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_getStatementValidator", "name": "API\\Resource\\V10\\Statements::getStatementValidator", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_setStatementValidator", "name": "API\\Resource\\V10\\Statements::setStatementValidator", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Resource\\V10\\Statements", "fromLink": "API/Resource/V10/Statements.html", "link": "API/Resource/V10/Statements.html#method_getOptions", "name": "API\\Resource\\V10\\Statements::getOptions", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API", "fromLink": "API.html", "link": "API/Service.html", "name": "API\\Service", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service.html#method___construct", "name": "API\\Service::__construct", "doc": ""Constructor.""},
- {"type": "Method", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service.html#method_getDocumentManager", "name": "API\\Service::getDocumentManager", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service.html#method_getSlim", "name": "API\\Service::getSlim", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service.html#method_setSlim", "name": "API\\Service::setSlim", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/Activity.html", "name": "API\\Service\\Activity", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_activityGet", "name": "API\\Service\\Activity::activityGet", "doc": ""Fetches activity profiles according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_getActivities", "name": "API\\Service\\Activity::getActivities", "doc": ""Gets the Activities.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_setActivities", "name": "API\\Service\\Activity::setActivities", "doc": ""Sets the Activities.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_getCursor", "name": "API\\Service\\Activity::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_setCursor", "name": "API\\Service\\Activity::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_getSingle", "name": "API\\Service\\Activity::getSingle", "doc": ""Gets the Is this a single activity fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Activity", "fromLink": "API/Service/Activity.html", "link": "API/Service/Activity.html#method_setSingle", "name": "API\\Service\\Activity::setSingle", "doc": ""Sets the Is this a single activity fetch?.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/ActivityProfile.html", "name": "API\\Service\\ActivityProfile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_activityProfileGet", "name": "API\\Service\\ActivityProfile::activityProfileGet", "doc": ""Fetches activity profiles according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_activityProfilePost", "name": "API\\Service\\ActivityProfile::activityProfilePost", "doc": ""Tries to save (merge) an activityProfile.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_activityProfilePut", "name": "API\\Service\\ActivityProfile::activityProfilePut", "doc": ""Tries to PUT (replace) an activityState.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_activityProfileDelete", "name": "API\\Service\\ActivityProfile::activityProfileDelete", "doc": ""Fetches activity states according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_getActivityProfiles", "name": "API\\Service\\ActivityProfile::getActivityProfiles", "doc": ""Gets the Activity states.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_setActivityProfiles", "name": "API\\Service\\ActivityProfile::setActivityProfiles", "doc": ""Sets the Activity profiles.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_getCursor", "name": "API\\Service\\ActivityProfile::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_setCursor", "name": "API\\Service\\ActivityProfile::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_getSingle", "name": "API\\Service\\ActivityProfile::getSingle", "doc": ""Gets the Is this a single activity state fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityProfile", "fromLink": "API/Service/ActivityProfile.html", "link": "API/Service/ActivityProfile.html#method_setSingle", "name": "API\\Service\\ActivityProfile::setSingle", "doc": ""Sets the Is this a single activity state fetch?.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/ActivityState.html", "name": "API\\Service\\ActivityState", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_activityStateGet", "name": "API\\Service\\ActivityState::activityStateGet", "doc": ""Fetches activity states according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_activityStatePost", "name": "API\\Service\\ActivityState::activityStatePost", "doc": ""Tries to save (merge) an activityState.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_activityStatePut", "name": "API\\Service\\ActivityState::activityStatePut", "doc": ""Tries to PUT (replace) an activityState.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_activityStateDelete", "name": "API\\Service\\ActivityState::activityStateDelete", "doc": ""Fetches activity states according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_getActivityStates", "name": "API\\Service\\ActivityState::getActivityStates", "doc": ""Gets the Activity states.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_setActivityStates", "name": "API\\Service\\ActivityState::setActivityStates", "doc": ""Sets the Activity states.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_getCursor", "name": "API\\Service\\ActivityState::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_setCursor", "name": "API\\Service\\ActivityState::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_getSingle", "name": "API\\Service\\ActivityState::getSingle", "doc": ""Gets the Is this a single activity state fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\ActivityState", "fromLink": "API/Service/ActivityState.html", "link": "API/Service/ActivityState.html#method_setSingle", "name": "API\\Service\\ActivityState::setSingle", "doc": ""Sets the Is this a single activity state fetch?.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/AgentProfile.html", "name": "API\\Service\\AgentProfile", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_agentProfileGet", "name": "API\\Service\\AgentProfile::agentProfileGet", "doc": ""Fetches agent profiles according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_agentProfilePost", "name": "API\\Service\\AgentProfile::agentProfilePost", "doc": ""Tries to save (merge) an agentProfile.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_agentProfilePut", "name": "API\\Service\\AgentProfile::agentProfilePut", "doc": ""Tries to PUT (replace) an agentProfile.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_agentProfileDelete", "name": "API\\Service\\AgentProfile::agentProfileDelete", "doc": ""Fetches activity states according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_getAgentProfiles", "name": "API\\Service\\AgentProfile::getAgentProfiles", "doc": ""Gets the Agent profiles.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_setAgentProfiles", "name": "API\\Service\\AgentProfile::setAgentProfiles", "doc": ""Sets the Agent profiles.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_getCursor", "name": "API\\Service\\AgentProfile::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_setCursor", "name": "API\\Service\\AgentProfile::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_getSingle", "name": "API\\Service\\AgentProfile::getSingle", "doc": ""Gets the Is this a single agent profile fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\AgentProfile", "fromLink": "API/Service/AgentProfile.html", "link": "API/Service/AgentProfile.html#method_setSingle", "name": "API\\Service\\AgentProfile::setSingle", "doc": ""Sets the Is this a single agent profile fetch?.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/Attachment.html", "name": "API\\Service\\Attachment", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Attachment", "fromLink": "API/Service/Attachment.html", "link": "API/Service/Attachment.html#method_fetchMetadataBySha2", "name": "API\\Service\\Attachment::fetchMetadataBySha2", "doc": ""Fetches file metadata from Mongo.""},
- {"type": "Method", "fromName": "API\\Service\\Attachment", "fromLink": "API/Service/Attachment.html", "link": "API/Service/Attachment.html#method_fetchFileBySha2", "name": "API\\Service\\Attachment::fetchFileBySha2", "doc": ""Fetches the actual file from the filesystem.""},
-
- {"type": "Class", "fromName": "API\\Service\\Auth", "fromLink": "API/Service/Auth.html", "link": "API/Service/Auth/AuthInterface.html", "name": "API\\Service\\Auth\\AuthInterface", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\AuthInterface", "fromLink": "API/Service/Auth/AuthInterface.html", "link": "API/Service/Auth/AuthInterface.html#method_extractToken", "name": "API\\Service\\Auth\\AuthInterface::extractToken", "doc": ""Fetches the token document, parsing it from the request.""},
-
- {"type": "Class", "fromName": "API\\Service\\Auth", "fromLink": "API/Service/Auth.html", "link": "API/Service/Auth/Basic.html", "name": "API\\Service\\Auth\\Basic", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_addToken", "name": "API\\Service\\Auth\\Basic::addToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_fetchToken", "name": "API\\Service\\Auth\\Basic::fetchToken", "doc": ""[fetchToken description].""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_deleteToken", "name": "API\\Service\\Auth\\Basic::deleteToken", "doc": ""[deleteToken description].""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_expireToken", "name": "API\\Service\\Auth\\Basic::expireToken", "doc": ""[expireToken description].""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_fetchTokens", "name": "API\\Service\\Auth\\Basic::fetchTokens", "doc": ""[fetchTokens description].""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_getScopeByName", "name": "API\\Service\\Auth\\Basic::getScopeByName", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_accessTokenGet", "name": "API\\Service\\Auth\\Basic::accessTokenGet", "doc": ""Tries to get an access token.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_accessTokenPost", "name": "API\\Service\\Auth\\Basic::accessTokenPost", "doc": ""Tries to create a new access token.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_accessTokenDelete", "name": "API\\Service\\Auth\\Basic::accessTokenDelete", "doc": ""Tries to delete an access token.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_extractToken", "name": "API\\Service\\Auth\\Basic::extractToken", "doc": ""Fetches the token document, parsing it from the request.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_getAccessTokens", "name": "API\\Service\\Auth\\Basic::getAccessTokens", "doc": ""Gets the Access tokens.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_setAccessTokens", "name": "API\\Service\\Auth\\Basic::setAccessTokens", "doc": ""Sets the Access tokens.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_getCursor", "name": "API\\Service\\Auth\\Basic::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_setCursor", "name": "API\\Service\\Auth\\Basic::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_getSingle", "name": "API\\Service\\Auth\\Basic::getSingle", "doc": ""Gets the Is this a single access token fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\Basic", "fromLink": "API/Service/Auth/Basic.html", "link": "API/Service/Auth/Basic.html#method_setSingle", "name": "API\\Service\\Auth\\Basic::setSingle", "doc": ""Sets the Is this a single access token fetch?.""},
-
- {"type": "Class", "fromName": "API\\Service\\Auth", "fromLink": "API/Service/Auth.html", "link": "API/Service/Auth/Exception.html", "name": "API\\Service\\Auth\\Exception", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Service\\Auth", "fromLink": "API/Service/Auth.html", "link": "API/Service/Auth/OAuth.html", "name": "API\\Service\\Auth\\OAuth", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_addToken", "name": "API\\Service\\Auth\\OAuth::addToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_fetchToken", "name": "API\\Service\\Auth\\OAuth::fetchToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_deleteToken", "name": "API\\Service\\Auth\\OAuth::deleteToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_expireToken", "name": "API\\Service\\Auth\\OAuth::expireToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_addClient", "name": "API\\Service\\Auth\\OAuth::addClient", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_fetchClients", "name": "API\\Service\\Auth\\OAuth::fetchClients", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_addScope", "name": "API\\Service\\Auth\\OAuth::addScope", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_authorizeGet", "name": "API\\Service\\Auth\\OAuth::authorizeGet", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_authorizePost", "name": "API\\Service\\Auth\\OAuth::authorizePost", "doc": ""POST authorize data.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_accessTokenPost", "name": "API\\Service\\Auth\\OAuth::accessTokenPost", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_extractToken", "name": "API\\Service\\Auth\\OAuth::extractToken", "doc": ""Fetches the token document, parsing it from the request.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getAccessTokens", "name": "API\\Service\\Auth\\OAuth::getAccessTokens", "doc": ""Gets the Access tokens.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setAccessTokens", "name": "API\\Service\\Auth\\OAuth::setAccessTokens", "doc": ""Sets the Access tokens.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getCursor", "name": "API\\Service\\Auth\\OAuth::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setCursor", "name": "API\\Service\\Auth\\OAuth::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getSingle", "name": "API\\Service\\Auth\\OAuth::getSingle", "doc": ""Gets the Is this a single access token fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setSingle", "name": "API\\Service\\Auth\\OAuth::setSingle", "doc": ""Sets the Is this a single access token fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getClient", "name": "API\\Service\\Auth\\OAuth::getClient", "doc": ""Gets the The relevant client(s).""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setClient", "name": "API\\Service\\Auth\\OAuth::setClient", "doc": ""Sets the The relevant client(s).""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getScopes", "name": "API\\Service\\Auth\\OAuth::getScopes", "doc": ""Gets the The relevant scopes.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setScopes", "name": "API\\Service\\Auth\\OAuth::setScopes", "doc": ""Sets the The relevant scopes.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getToken", "name": "API\\Service\\Auth\\OAuth::getToken", "doc": ""Gets the The relevant token.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setToken", "name": "API\\Service\\Auth\\OAuth::setToken", "doc": ""Sets the The relevant token.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_getRedirectUri", "name": "API\\Service\\Auth\\OAuth::getRedirectUri", "doc": ""Gets the The relevant redirectUri.""},
- {"type": "Method", "fromName": "API\\Service\\Auth\\OAuth", "fromLink": "API/Service/Auth/OAuth.html", "link": "API/Service/Auth/OAuth.html#method_setRedirectUri", "name": "API\\Service\\Auth\\OAuth::setRedirectUri", "doc": ""Sets the The relevant redirectUri.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/Exception.html", "name": "API\\Service\\Exception", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/Statement.html", "name": "API\\Service\\Statement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_statementGet", "name": "API\\Service\\Statement::statementGet", "doc": ""Fetches statements according to the given parameters.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_statementPost", "name": "API\\Service\\Statement::statementPost", "doc": ""Tries to a statement with a specified statementId.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_statementPut", "name": "API\\Service\\Statement::statementPut", "doc": ""Tries to PUT a statement with a specified statementId.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getStatements", "name": "API\\Service\\Statement::getStatements", "doc": ""Gets the Statements.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setStatements", "name": "API\\Service\\Statement::setStatements", "doc": ""Sets the Statements.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getAttachments", "name": "API\\Service\\Statement::getAttachments", "doc": ""Gets the Attachments.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setAttachments", "name": "API\\Service\\Statement::setAttachments", "doc": ""Sets the Attachments.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getLimit", "name": "API\\Service\\Statement::getLimit", "doc": ""Gets the The limit associated with the document query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setLimit", "name": "API\\Service\\Statement::setLimit", "doc": ""Sets the The limit associated with the document query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getFormat", "name": "API\\Service\\Statement::getFormat", "doc": ""Gets the Format associated with the query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setFormat", "name": "API\\Service\\Statement::setFormat", "doc": ""Sets the Format associated with the query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getDescending", "name": "API\\Service\\Statement::getDescending", "doc": ""Gets the Descending order associated with the query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setDescending", "name": "API\\Service\\Statement::setDescending", "doc": ""Sets the Descending order associated with the query.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getCursor", "name": "API\\Service\\Statement::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setCursor", "name": "API\\Service\\Statement::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getSingle", "name": "API\\Service\\Statement::getSingle", "doc": ""Gets the Is this a single statement fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setSingle", "name": "API\\Service\\Statement::setSingle", "doc": ""Sets the Is this a single statement fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getMatch", "name": "API\\Service\\Statement::getMatch", "doc": ""Gets the Is this a single statement match?.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setMatch", "name": "API\\Service\\Statement::setMatch", "doc": ""Sets the Is this a single statement match?.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getAccessToken", "name": "API\\Service\\Statement::getAccessToken", "doc": ""Gets the Access token to check for permissions.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_getCount", "name": "API\\Service\\Statement::getCount", "doc": ""Gets the Provide a statement count.""},
- {"type": "Method", "fromName": "API\\Service\\Statement", "fromLink": "API/Service/Statement.html", "link": "API/Service/Statement.html#method_setCount", "name": "API\\Service\\Statement::setCount", "doc": ""Sets the Provide a statement count.""},
-
- {"type": "Class", "fromName": "API\\Service", "fromLink": "API/Service.html", "link": "API/Service/User.html", "name": "API\\Service\\User", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_loginGet", "name": "API\\Service\\User::loginGet", "doc": ""Logs the user in.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_loginPost", "name": "API\\Service\\User::loginPost", "doc": ""Logs the user in.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_loggedIn", "name": "API\\Service\\User::loggedIn", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_addUser", "name": "API\\Service\\User::addUser", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_fetchAll", "name": "API\\Service\\User::fetchAll", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_fetchAvailablePermissions", "name": "API\\Service\\User::fetchAvailablePermissions", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_getUsers", "name": "API\\Service\\User::getUsers", "doc": ""Gets the Users.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_setUsers", "name": "API\\Service\\User::setUsers", "doc": ""Sets the Users.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_getCursor", "name": "API\\Service\\User::getCursor", "doc": ""Gets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_setCursor", "name": "API\\Service\\User::setCursor", "doc": ""Sets the Cursor.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_getSingle", "name": "API\\Service\\User::getSingle", "doc": ""Gets the Is this a single user fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_setSingle", "name": "API\\Service\\User::setSingle", "doc": ""Sets the Is this a single user fetch?.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_getErrors", "name": "API\\Service\\User::getErrors", "doc": ""Gets the Any errors that might've ocurred are stored here.""},
- {"type": "Method", "fromName": "API\\Service\\User", "fromLink": "API/Service/User.html", "link": "API/Service/User.html#method_setErrors", "name": "API\\Service\\User::setErrors", "doc": ""Sets the Any errors that might've ocurred are stored here.""},
-
- {"type": "Class", "fromName": "API\\Util", "fromLink": "API/Util.html", "link": "API/Util/Date.html", "name": "API\\Util\\Date", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Date", "fromLink": "API/Util/Date.html", "link": "API/Util/Date.html#method_dateStringToMongoDate", "name": "API\\Util\\Date::dateStringToMongoDate", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Date", "fromLink": "API/Util/Date.html", "link": "API/Util/Date.html#method_dateTimeToMongoDate", "name": "API\\Util\\Date::dateTimeToMongoDate", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Date", "fromLink": "API/Util/Date.html", "link": "API/Util/Date.html#method_dateTimeToISO8601", "name": "API\\Util\\Date::dateTimeToISO8601", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Date", "fromLink": "API/Util/Date.html", "link": "API/Util/Date.html#method_secondsUntil", "name": "API\\Util\\Date::secondsUntil", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Date", "fromLink": "API/Util/Date.html", "link": "API/Util/Date.html#method_dateFromSeconds", "name": "API\\Util\\Date::dateFromSeconds", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Util", "fromLink": "API/Util.html", "link": "API/Util/Filesystem.html", "name": "API\\Util\\Filesystem", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Filesystem", "fromLink": "API/Util/Filesystem.html", "link": "API/Util/Filesystem.html#method_generateAdapter", "name": "API\\Util\\Filesystem::generateAdapter", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\Filesystem", "fromLink": "API/Util/Filesystem.html", "link": "API/Util/Filesystem.html#method_generateSHA2", "name": "API\\Util\\Filesystem::generateSHA2", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Util", "fromLink": "API/Util.html", "link": "API/Util/OAuth.html", "name": "API\\Util\\OAuth", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\OAuth", "fromLink": "API/Util/OAuth.html", "link": "API/Util/OAuth.html#method_generateToken", "name": "API\\Util\\OAuth::generateToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\OAuth", "fromLink": "API/Util/OAuth.html", "link": "API/Util/OAuth.html#method_loadSession", "name": "API\\Util\\OAuth::loadSession", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Util\\OAuth", "fromLink": "API/Util/OAuth.html", "link": "API/Util/OAuth.html#method_generateCsrfToken", "name": "API\\Util\\OAuth::generateCsrfToken", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API", "fromLink": "API.html", "link": "API/Validator.html", "name": "API\\Validator", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method___construct", "name": "API\\Validator::__construct", "doc": ""Constructor.""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_getSchemaValidator", "name": "API\\Validator::getSchemaValidator", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_setSchemaValidator", "name": "API\\Validator::setSchemaValidator", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_getSchemaReferenceResolver", "name": "API\\Validator::getSchemaReferenceResolver", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_setSchemaReferenceResolver", "name": "API\\Validator::setSchemaReferenceResolver", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_getSchemaRetriever", "name": "API\\Validator::getSchemaRetriever", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_setSchemaRetriever", "name": "API\\Validator::setSchemaRetriever", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_setDefaultSchemaValidator", "name": "API\\Validator::setDefaultSchemaValidator", "doc": ""Sets the default schema validator.""},
- {"type": "Method", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator.html#method_validateRequest", "name": "API\\Validator::validateRequest", "doc": ""Performs general validation of the request.""},
-
- {"type": "Class", "fromName": "API\\Validator", "fromLink": "API/Validator.html", "link": "API/Validator/Exception.html", "name": "API\\Validator\\Exception", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Validator\\V10", "fromLink": "API/Validator/V10.html", "link": "API/Validator/V10/Attachment.html", "name": "API\\Validator\\V10\\Attachment", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\Validator\\V10", "fromLink": "API/Validator/V10.html", "link": "API/Validator/V10/Statement.html", "name": "API\\Validator\\V10\\Statement", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator\\V10\\Statement", "fromLink": "API/Validator/V10/Statement.html", "link": "API/Validator/V10/Statement.html#method_validateGetRequest", "name": "API\\Validator\\V10\\Statement::validateGetRequest", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator\\V10\\Statement", "fromLink": "API/Validator/V10/Statement.html", "link": "API/Validator/V10/Statement.html#method_validatePostRequest", "name": "API\\Validator\\V10\\Statement::validatePostRequest", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\Validator\\V10\\Statement", "fromLink": "API/Validator/V10/Statement.html", "link": "API/Validator/V10/Statement.html#method_validatePutRequest", "name": "API\\Validator\\V10\\Statement::validatePutRequest", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API", "fromLink": "API.html", "link": "API/View.html", "name": "API\\View", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View", "fromLink": "API/View.html", "link": "API/View.html#method___construct", "name": "API\\View::__construct", "doc": ""Construct.""},
- {"type": "Method", "fromName": "API\\View", "fromLink": "API/View.html", "link": "API/View.html#method_getSlim", "name": "API\\View::getSlim", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View", "fromLink": "API/View.html", "link": "API/View.html#method_setSlim", "name": "API\\View::setSlim", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/About.html", "name": "API\\View\\V10\\About", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\About", "fromLink": "API/View/V10/About.html", "link": "API/View/V10/About.html#method_render", "name": "API\\View\\V10\\About::render", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/Activity.html", "name": "API\\View\\V10\\Activity", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\Activity", "fromLink": "API/View/V10/Activity.html", "link": "API/View/V10/Activity.html#method_renderGetSingle", "name": "API\\View\\V10\\Activity::renderGetSingle", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/ActivityProfile.html", "name": "API\\View\\V10\\ActivityProfile", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/ActivityState.html", "name": "API\\View\\V10\\ActivityState", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/Agent.html", "name": "API\\View\\V10\\Agent", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\Agent", "fromLink": "API/View/V10/Agent.html", "link": "API/View/V10/Agent.html#method_renderGet", "name": "API\\View\\V10\\Agent::renderGet", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/AgentProfile.html", "name": "API\\View\\V10\\AgentProfile", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/BaseDocument.html", "name": "API\\View\\V10\\BaseDocument", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\BaseDocument", "fromLink": "API/View/V10/BaseDocument.html", "link": "API/View/V10/BaseDocument.html#method_renderGet", "name": "API\\View\\V10\\BaseDocument::renderGet", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\BaseDocument", "fromLink": "API/View/V10/BaseDocument.html", "link": "API/View/V10/BaseDocument.html#method_renderGetSingle", "name": "API\\View\\V10\\BaseDocument::renderGetSingle", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10\\BasicAuth", "fromLink": "API/View/V10/BasicAuth.html", "link": "API/View/V10/BasicAuth/AccessToken.html", "name": "API\\View\\V10\\BasicAuth\\AccessToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\BasicAuth\\AccessToken", "fromLink": "API/View/V10/BasicAuth/AccessToken.html", "link": "API/View/V10/BasicAuth/AccessToken.html#method_render", "name": "API\\View\\V10\\BasicAuth\\AccessToken::render", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10\\OAuth", "fromLink": "API/View/V10/OAuth.html", "link": "API/View/V10/OAuth/AccessToken.html", "name": "API\\View\\V10\\OAuth\\AccessToken", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\OAuth\\AccessToken", "fromLink": "API/View/V10/OAuth/AccessToken.html", "link": "API/View/V10/OAuth/AccessToken.html#method_renderGet", "name": "API\\View\\V10\\OAuth\\AccessToken::renderGet", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10\\OAuth", "fromLink": "API/View/V10/OAuth.html", "link": "API/View/V10/OAuth/Authorize.html", "name": "API\\View\\V10\\OAuth\\Authorize", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\OAuth\\Authorize", "fromLink": "API/View/V10/OAuth/Authorize.html", "link": "API/View/V10/OAuth/Authorize.html#method_renderGet", "name": "API\\View\\V10\\OAuth\\Authorize::renderGet", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10\\OAuth", "fromLink": "API/View/V10/OAuth.html", "link": "API/View/V10/OAuth/Login.html", "name": "API\\View\\V10\\OAuth\\Login", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\OAuth\\Login", "fromLink": "API/View/V10/OAuth/Login.html", "link": "API/View/V10/OAuth/Login.html#method_renderGet", "name": "API\\View\\V10\\OAuth\\Login::renderGet", "doc": ""\n""},
-
- {"type": "Class", "fromName": "API\\View\\V10", "fromLink": "API/View/V10.html", "link": "API/View/V10/Statements.html", "name": "API\\View\\V10\\Statements", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\Statements", "fromLink": "API/View/V10/Statements.html", "link": "API/View/V10/Statements.html#method_renderGet", "name": "API\\View\\V10\\Statements::renderGet", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\Statements", "fromLink": "API/View/V10/Statements.html", "link": "API/View/V10/Statements.html#method_renderGetSingle", "name": "API\\View\\V10\\Statements::renderGetSingle", "doc": ""\n""},
- {"type": "Method", "fromName": "API\\View\\V10\\Statements", "fromLink": "API/View/V10/Statements.html", "link": "API/View/V10/Statements.html#method_renderPost", "name": "API\\View\\V10\\Statements::renderPost", "doc": ""\n""},
-
-
- // Fix trailing commas in the index
- {}
- ];
-
- /** Tokenizes strings by namespaces and functions */
- function tokenizer(term) {
- if (!term) {
- return [];
- }
-
- var tokens = [term];
- var meth = term.indexOf('::');
-
- // Split tokens into methods if "::" is found.
- if (meth > -1) {
- tokens.push(term.substr(meth + 2));
- term = term.substr(0, meth - 2);
- }
-
- // Split by namespace or fake namespace.
- if (term.indexOf('\\') > -1) {
- tokens = tokens.concat(term.split('\\'));
- } else if (term.indexOf('_') > 0) {
- tokens = tokens.concat(term.split('_'));
- }
-
- // Merge in splitting the string by case and return
- tokens = tokens.concat(term.match(/(([A-Z]?[^A-Z]*)|([a-z]?[^a-z]*))/g).slice(0,-1));
-
- return tokens;
- };
-
- root.Sami = {
- /**
- * Cleans the provided term. If no term is provided, then one is
- * grabbed from the query string "search" parameter.
- */
- cleanSearchTerm: function(term) {
- // Grab from the query string
- if (typeof term === 'undefined') {
- var name = 'search';
- var regex = new RegExp("[\\?&]" + name + "=([^]*)");
- var results = regex.exec(location.search);
- if (results === null) {
- return null;
- }
- term = decodeURIComponent(results[1].replace(/\+/g, " "));
- }
-
- return term.replace(/<(?:.|\n)*?>/gm, '');
- },
-
- /** Searches through the index for a given term */
- search: function(term) {
- // Create a new search index if needed
- if (!bhIndex) {
- bhIndex = new Bloodhound({
- limit: 500,
- local: searchIndex,
- datumTokenizer: function (d) {
- return tokenizer(d.name);
- },
- queryTokenizer: Bloodhound.tokenizers.whitespace
- });
- bhIndex.initialize();
- }
-
- results = [];
- bhIndex.get(term, function(matches) {
- results = matches;
- });
-
- if (!rootPath) {
- return results;
- }
-
- // Fix the element links based on the current page depth.
- return $.map(results, function(ele) {
- if (ele.link.indexOf('..') > -1) {
- return ele;
- }
- ele.link = rootPath + ele.link;
- if (ele.fromLink) {
- ele.fromLink = rootPath + ele.fromLink;
- }
- return ele;
- });
- },
-
- /** Get a search class for a specific type */
- getSearchClass: function(type) {
- return searchTypeClasses[type] || searchTypeClasses['_'];
- },
-
- /** Add the left-nav tree to the site */
- injectApiTree: function(ele) {
- ele.html(treeHtml);
- }
- };
-
- $(function() {
- // Modify the HTML to work correctly based on the current depth
- rootPath = $('body').attr('data-root-path');
- treeHtml = treeHtml.replace(/href="/g, 'href="' + rootPath);
- Sami.injectApiTree($('#api-tree'));
- });
-
- return root.Sami;
-})(window);
-
-$(function() {
-
- // Enable the version switcher
- $('#version-switcher').change(function() {
- window.location = $(this).val()
- });
-
-
- // Toggle left-nav divs on click
- $('#api-tree .hd span').click(function() {
- $(this).parent().parent().toggleClass('opened');
- });
-
- // Expand the parent namespaces of the current page.
- var expected = $('body').attr('data-name');
-
- if (expected) {
- // Open the currently selected node and its parents.
- var container = $('#api-tree');
- var node = $('#api-tree li[data-name="' + expected + '"]');
- // Node might not be found when simulating namespaces
- if (node.length > 0) {
- node.addClass('active').addClass('opened');
- node.parents('li').addClass('opened');
- var scrollPos = node.offset().top - container.offset().top + container.scrollTop();
- // Position the item nearer to the top of the screen.
- scrollPos -= 200;
- container.scrollTop(scrollPos);
- }
- }
-
-
-
- var form = $('#search-form .typeahead');
- form.typeahead({
- hint: true,
- highlight: true,
- minLength: 1
- }, {
- name: 'search',
- displayKey: 'name',
- source: function (q, cb) {
- cb(Sami.search(q));
- }
- });
-
- // The selection is direct-linked when the user selects a suggestion.
- form.on('typeahead:selected', function(e, suggestion) {
- window.location = suggestion.link;
- });
-
- // The form is submitted when the user hits enter.
- form.keypress(function (e) {
- if (e.which == 13) {
- $('#search-form').submit();
- return true;
- }
- });
-
-
-});
-
-
diff --git a/docs/search.html b/docs/search.html
deleted file mode 100644
index b7890491..00000000
--- a/docs/search.html
+++ /dev/null
@@ -1,152 +0,0 @@
-
-
-
-
-
- Search | API
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This page allows you to search through the API documentation for
- specific terms. Enter your search words into the box below and click
- "submit". The search will be performed on namespaces, clases, interfaces,
- traits, functions, and methods.
-
-
-
-
Search Results
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/traits.html b/docs/traits.html
deleted file mode 100644
index e4f8060b..00000000
--- a/docs/traits.html
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
- Traits | API
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/generate-docs.sh b/generate-docs.sh
new file mode 100755
index 00000000..90675b66
--- /dev/null
+++ b/generate-docs.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+PHAR_FILE='sami.phar'
+PHAR_URI='http://get.sensiolabs.org/'
+CONFIG_FILE='sami.config.php'
+
+RED='\033[00;31m'
+GREEN='\033[00;32m'
+YELLOW='\033[00;33m'
+GREY='\033[00;37m'
+CLEAR='\033[00;39m'
+
+echo ${YELLOW}
+echo "----------------------- "
+echo " (1/2) Downloading ${PHAR_URI}${PHAR_FILE}.."
+echo "----------------------- "
+echo ${CLEAR}
+
+curl -O ${PHAR_URI}${PHAR_FILE}
+
+if [ ! -e sami.phar ]
+then
+ echo "${RED}-----------------------${CLEAR}"
+ echo "${RED}ERROR$ downloading${CLEAR} ${PHAR_URI}${PHAR_FILE}"
+ echo " - download ${PHAR_FILE} manually from: ${GREY}https://github.com/FriendsOfPHP/Sami${CLEAR}"
+ echo " - run: ${GREY}php sami.phar update ${CONFIG_FILE}${CLEAR}"
+ echo "${RED}-----------------------${CLEAR}"
+ exit 1
+fi
+
+echo ${YELLOW}
+echo "----------------------- "
+echo "Compiling docs.. (using ${CONFIG_FILE})"
+echo "----------------------- "
+echo ${CLEAR}
+
+php sami.phar update ${CONFIG_FILE}
+
+echo ${GREEN}
+echo "----------------------- "
+echo "Docs compiled to ${GREY}./docs/${CLEAR}"
+echo "----------------------- "
+echo ${CLEAR}
diff --git a/migrations/20150906233033_StatementRefs.php b/migrations/20150906233033_StatementRefs.php
index 4aae8e3d..5df1c13f 100644
--- a/migrations/20150906233033_StatementRefs.php
+++ b/migrations/20150906233033_StatementRefs.php
@@ -1,7 +1,6 @@
+
+
+
+ ./tests
+
+
+
+
+
+
diff --git a/public/assets/styles/main.css b/public/assets/styles/main.css
index 6a4ba7fc..759a7cc8 100644
--- a/public/assets/styles/main.css
+++ b/public/assets/styles/main.css
@@ -1,5 +1,5 @@
body{
- font-family: 'Roboto Condensed', sans-serif;
+ font-family: 'Raleway', sans-serif;
background-color: #f9f9f9;
color: rgb(85, 86, 90);
padding-bottom: 2em;
@@ -7,6 +7,9 @@ body{
.text-center{
text-align: center;
}
+.text-center{
+ margin-top: 1em;
+}
/**
* Branding
@@ -80,3 +83,21 @@ pure-button{
padding: 2em 1em;
border-radius: 5px;
}
+#id_username, #id_password, #id_remember_me, #id_login{
+ margin-left: auto;
+ margin-right: auto;
+ min-width: 200px;
+ max-width: 250px;
+}
+.pure-form .pure-control-group label{
+ margin-left: auto;
+ margin-right: auto;
+ min-width: 200px;
+ max-width: 250px;
+}
+.pure-form .pure-control-group button{
+ margin-left: auto;
+ margin-right: auto;
+ min-width: 200px;
+ max-width: 250px;
+}
diff --git a/public/assets/styles/pure/pure-min.css b/public/assets/styles/pure/pure-min.css
index b169fec3..a38ac950 100644
--- a/public/assets/styles/pure/pure-min.css
+++ b/public/assets/styles/pure/pure-min.css
@@ -1,6 +1,6 @@
/*!
-Pure v0.6.0
-Copyright 2014 Yahoo! Inc. All rights reserved.
+Pure v1.0.0
+Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/yahoo/pure/blob/master/LICENSE.md
*/
@@ -8,5 +8,5 @@ https://github.com/yahoo/pure/blob/master/LICENSE.md
normalize.css v^3.0 | MIT License | git.io/normalize
Copyright (c) Nicolas Gallagher and Jonathan Neal
*/
-/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
-html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}
\ No newline at end of file
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+.pure-button:focus,a:active,a:hover{outline:0}.pure-table,table{border-collapse:collapse;border-spacing:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.pure-button,input{line-height:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}.pure-button,.pure-form input:not([type]),.pure-menu{box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend,td,th{padding:0}legend{border:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u,.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block;zoom:1}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:transparent;background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{filter:alpha(opacity=90);background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px}.pure-form input[type=color]{padding:.2em .5em}.pure-form input:not([type]):focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=text]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=checkbox]:focus,.pure-form input[type=radio]:focus{outline:#129FEA auto 1px}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input:not([type])[disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=text][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input:not([type]),.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=text],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=tel],.pure-group input[type=color],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=text]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td,.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}
\ No newline at end of file
diff --git a/public/index.php b/public/index.php
index 6783123e..1e620967 100644
--- a/public/index.php
+++ b/public/index.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,267 +25,8 @@
// Require the autoloader
require __DIR__.'/../vendor/autoload.php';
-use Slim\Slim;
-use BurningDiode\Slim\Config as Config;
-use Flynsarmy\SlimMonolog\Log as Logger;
-use Monolog\Handler\StreamHandler;
-use API\Resource;
-use League\Url\Url;
-use Slim\Helper\Set;
-use Sokil\Mongo\Client;
-use API\Service\Auth\OAuth as OAuthService;
-use API\Service\Auth\Basic as BasicAuthService;
-use API\Service\Log as LogService;
-use Slim\Views\Twig;
-use API\Service\Auth\Exception as AuthFailureException;
-use API\Util\Versioning;
+use API\Bootstrap;
-// Set up a new Slim instance - default mode is production (it is overriden with SLIM_MODE environment variable)
-$app = new Slim();
-
-$appRoot = dirname(__DIR__);
-
-// Prepare config loader
-Config\Yaml::getInstance()->addParameters(['app.root' => $appRoot]);
-// Default config
-try {
- Config\Yaml::getInstance()->addFile($appRoot.'/src/xAPI/Config/Config.yml');
-} catch (\Exception $e) {
- if (PHP_SAPI === 'cli' && ((isset($argv[1]) && $argv[1] === 'setup:db') || (isset($argv[0]) && !isset($argv[1])))) {
- // Database setup in progress, ignore exception
- } else {
- throw new \Exception('You must run the setup:db command using the X CLI tool!');
- }
-}
-
-// Use Mongo's native long int
-ini_set('mongo.native_long', 1);
-
-// Only invoked if mode is "production"
-$app->configureMode('production', function () use ($app, $appRoot) {
- // Add config
- Config\Yaml::getInstance()->addFile($appRoot.'/src/xAPI/Config/Config.production.yml');
-
- // Debug mode
- if ($app->debug) {
- $app->log->setLevel(\Slim\Log::DEBUG);
- }
-
- // Set up logging
- $logger = new Logger\MonologWriter([
- 'handlers' => [
- new StreamHandler($appRoot.'/storage/logs/production.'.date('Y-m-d').'.log'),
- ],
- ]);
-
- $app->config('log.writer', $logger);
-});
-
-// Only invoked if mode is "development"
-$app->configureMode('development', function () use ($app, $appRoot) {
- // Add config
- Config\Yaml::getInstance()->addFile($appRoot.'/src/xAPI/Config/Config.development.yml');
-
- // Debug mode
- if ($app->debug) {
- $app->log->setLevel(\Slim\Log::DEBUG);
- }
-
- // Set up logging
- $logger = new Logger\MonologWriter([
- 'handlers' => [
- new StreamHandler($appRoot.'/storage/logs/development.'.date('Y-m-d').'.log'),
- ],
- ]);
-
- $app->config('log.writer', $logger);
-});
-
-if (PHP_SAPI !== 'cli') {
- $app->url = Url::createFromServer($_SERVER);
-}
-
-// Error handling
-$app->error(function (\Exception $e) {
- $code = $e->getCode();
- if ($code < 100) {
- $code = 500;
- }
- Resource::error($code, $e->getMessage());
-});
-
-// Database layer setup
-$app->hook('slim.before', function () use ($app) {
-
- // #91, Slim 2 cannot deal with full uri's in $_SERVER['REQUEST_URI']
- // TODO: remove in Slim 3 ?
- $info = $app->environment()['PATH_INFO'];
- $url = $app->request->getUrl();
- if (stripos($info, $url) !== false) {
- $url .= (substr($url, -1) == '/' ? '' : '/');
- $app->environment()['PATH_INFO'] = str_replace($url, '', $info);
- }
-
- $app->container->singleton('mongo', function () use ($app) {
- $client = new Client($app->config('database')['host_uri']);
- $client->map([
- $app->config('database')['db_name'] => '\API\Collection',
- ]);
- $client->useDatabase($app->config('database')['db_name']);
-
- return $client;
- });
-});
-
-// CORS compatibility layer (Internet Explorer)
-$app->hook('slim.before.router', function () use ($app) {
- if ($app->request->isPost() && $app->request->get('method')) {
- $method = $app->request->get('method');
- $app->environment()['REQUEST_METHOD'] = strtoupper($method);
- mb_parse_str($app->request->getBody(), $postData);
- $parameters = new Set($postData);
- if ($parameters->has('content')) {
- $content = $parameters->get('content');
- $app->environment()['slim.input'] = $content;
- $parameters->remove('content');
- } else {
- // Content is the only valid body parameter...everything else are either headers or query parameters
- $app->environment()['slim.input'] = '';
- }
- $app->request->headers->replace($parameters->all());
- $app->environment()['slim.request.query_hash'] = $parameters->all();
- }
-});
-
-// Parse version
-$app->hook('slim.before.dispatch', function () use ($app, $appRoot) {
- // Version
- $app->container->singleton('version', function () use ($app) {
- if ($app->request->isOptions() || $app->request->getPathInfo() === '/about' || strpos(strtolower($app->request->getPathInfo()), '/oauth') === 0) {
- $versionString = $app->config('xAPI')['latest_version'];
- } else {
- $versionString = $app->request->headers('X-Experience-API-Version');
- }
-
- if ($versionString === null) {
- throw new \Exception('X-Experience-API-Version header missing.', Resource::STATUS_BAD_REQUEST);
- } else {
- try {
- $version = Versioning::fromString($versionString);
- } catch (\InvalidArgumentException $e) {
- throw new \Exception('X-Experience-API-Version header invalid.', Resource::STATUS_BAD_REQUEST);
- }
-
- if (!in_array($versionString, $app->config('xAPI')['supported_versions'])) {
- throw new \Exception('X-Experience-API-Version is not supported.', Resource::STATUS_BAD_REQUEST);
- }
-
- return $version;
- }
- });
-
- // Request logging
- $app->container->singleton('requestLog', function () use ($app) {
- $logService = new LogService($app);
- $logDocument = $logService->logRequest($app->request);
-
- return $logDocument;
- });
-
- // Auth - token
- $app->container->singleton('auth', function () use ($app) {
- if (!$app->request->isOptions() && !($app->request->getPathInfo() === '/about')) {
- $basicAuthService = new BasicAuthService($app);
- $oAuthService = new OAuthService($app);
-
- $token = null;
-
- try {
- $token = $oAuthService->extractToken($app->request);
- $app->requestLog->addRelation('oAuthToken', $token)->save();
- } catch (AuthFailureException $e) {
- // Ignore
- }
-
- try {
- $token = $basicAuthService->extractToken($app->request);
- $app->requestLog->addRelation('basicToken', $token)->save();
- } catch (AuthFailureException $e) {
- // Ignore
- }
-
- if (null === $token) {
- throw new \Exception('Credentials invalid!', Resource::STATUS_UNAUTHORIZED);
- }
-
- return $token;
- }
- });
-
- // Load Twig only if this is a request where we actually need it!
- if (strpos(strtolower($app->request->getPathInfo()), '/oauth') === 0) {
- $twigContainer = new Twig();
- $app->container->singleton('view', function () use ($twigContainer) {
- return $twigContainer;
- });
- $app->view->parserOptions['cache'] = $appRoot.'/storage/.Cache';
- }
-
- // Content type check
- if (($app->request->isPost() || $app->request->isPut()) && $app->request->getPathInfo() === '/statements' && !in_array($app->request->getMediaType(), ['application/json', 'multipart/mixed', 'application/x-www-form-urlencoded'])) {
- // Bad Content-Type
- throw new \Exception('Bad Content-Type.', Resource::STATUS_BAD_REQUEST);
- }
-});
-
-// Start with routing - dynamic for now
-// Get
-$app->get('/:resource(/(:action)(/))', function ($resource, $subResource = null) use ($app) {
- $resource = Resource::load($app->version, $resource, $subResource);
- if ($resource === null) {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
- } else {
- $resource->get();
- }
-});
-// Post
-$app->post('/:resource(/(:action)(/))', function ($resource, $subResource = null) use ($app) {
- $resource = Resource::load($app->version, $resource, $subResource);
- if ($resource === null) {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
- } else {
- $resource->post();
- }
-});
-// Put
-$app->put('/:resource(/(:action)(/))', function ($resource, $subResource = null) use ($app) {
- $resource = Resource::load($app->version, $resource, $subResource);
- if ($resource === null) {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
- } else {
- $resource->put();
- }
-});
-// Delete
-$app->delete('/:resource(/(:action)(/))', function ($resource, $subResource = null) use ($app) {
- $resource = Resource::load($app->version, $resource, $subResource);
- if ($resource === null) {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
- } else {
- $resource->delete();
- }
-});
-// Options
-$app->options('/:resource(/(:action)(/))', function ($resource, $subResource = null) use ($app) {
- $resource = Resource::load($app->version, $resource, $subResource);
- if ($resource === null) {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
- } else {
- $resource->options();
- }
-});
-// Not found
-$app->notFound(function () {
- Resource::error(Resource::STATUS_NOT_FOUND, 'Cannot find requested resource.');
-});
+$bootstrapper = Bootstrap::factory(Bootstrap::Web);
+$app = $bootstrapper->bootWebApp();
$app->run();
diff --git a/sami.config.php b/sami.config.php
new file mode 100644
index 00000000..a86d5e97
--- /dev/null
+++ b/sami.config.php
@@ -0,0 +1,37 @@
+files()
+ ->name('*.php')
+ ->in(LXHIVE_DOCS_SRC);
+
+// options
+$sami = new Sami($iterator, array(
+ 'title' => 'lxHive',
+ 'build_dir' => LXHIVE_DOCS_BUILD,
+ 'cache_dir' => LXHIVE_DOCS_BUILD,
+ 'default_opened_level' => 2,
+));
+
+// Document private and protected functions/properties
+$sami['filter'] = function () {
+ return new TrueFilter();
+};
+
+return $sami;
diff --git a/src/xAPI/Collection/ActivityStates.php b/src/xAPI/Admin.php
similarity index 73%
rename from src/xAPI/Collection/ActivityStates.php
rename to src/xAPI/Admin.php
index aff7bbb2..fd88e9b9 100644
--- a/src/xAPI/Collection/ActivityStates.php
+++ b/src/xAPI/Admin.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,14 +22,21 @@
* file that was distributed with this source code.
*/
-namespace API\Collection;
+namespace API;
-use Sokil\Mongo\Collection;
-
-class ActivityStates extends Collection
+/**
+ * Base Admin class
+ */
+abstract class Admin
{
- public function getDocumentClassName(array $documentData = null)
+ use BaseTrait;
+
+ /**
+ * constructor
+ * @param \Psr\Container\ContainerInterface $container
+ */
+ public function __construct($container)
{
- return '\\API\\Document\\ActivityState';
+ $this->setContainer($container);
}
}
diff --git a/src/xAPI/Admin/AdminException.php b/src/xAPI/Admin/AdminException.php
new file mode 100644
index 00000000..6155a027
--- /dev/null
+++ b/src/xAPI/Admin/AdminException.php
@@ -0,0 +1,30 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+namespace API\Admin;
+
+/**
+ * Exceptions for Admin API
+ */
+class AdminException extends \RunTimeException
+{
+}
diff --git a/src/xAPI/Admin/Auth.php b/src/xAPI/Admin/Auth.php
new file mode 100644
index 00000000..938a5422
--- /dev/null
+++ b/src/xAPI/Admin/Auth.php
@@ -0,0 +1,142 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Admin;
+
+use API\Service\Auth\OAuth as OAuthService;
+use API\Service\Auth\Basic as BasicAuthService;
+use API\Admin;
+
+/**
+ * Auth Managment
+ */
+class Auth extends Admin
+{
+ /**
+ * Fetches a list of all oAuth Clients
+ *
+ * @return array
+ */
+ public function listOAuthClients()
+ {
+ $oAuthService = new OAuthService($this->getContainer());
+ $documentResult = $oAuthService->fetchClients();
+ $textArray = $documentResult->getCursor()->toArray();
+
+ return $textArray;
+ }
+
+ /**
+ * Add an oAuth client record
+ * @param string $name
+ * @param string $description
+ * @param string $redirectUri
+ *
+ * @return \API\Document\Generic
+ */
+ public function addOAuthClient($name, $description, $redirectUri)
+ {
+ $oAuthService = new OAuthService($this->getContainer());
+ $client = $oAuthService->addClient($name, $description, $redirectUri);
+
+ return $client;
+ }
+
+ /**
+ * Fetches a list of all basic tokens
+ *
+ * @return array
+ */
+ public function listBasicTokens()
+ {
+ $accessTokenService = new BasicAuthService($this->getContainer());
+ $tokens = $accessTokenService->fetchTokens();
+ $textArray = [];
+ foreach ($tokens as $document) {
+ $textArray[] = $document;
+ }
+
+ return $textArray;
+ }
+
+ /**
+ * Fetches a list of all basic token id's
+ *
+ * @return array
+ */
+ public function listBasicTokenIds()
+ {
+ $accessTokenService = new BasicAuthService($this->getContainer());
+ $tokens = $accessTokenService->fetchTokens();
+ $keys = [];
+ foreach ($tokens as $document) {
+ $keys[] = $document->key;
+ }
+
+ return $keys;
+ }
+
+ /**
+ * Expire a basic token
+ * @param string $clientId valid clientId
+ *
+ * @return void
+ */
+ public function expireBasicToken($key)
+ {
+ $accessTokenService = new BasicAuthService($this->getContainer());
+ $accessTokenService->expireToken($key);
+ }
+
+ /**
+ * Deleta a basic token
+ * @param string $clientId valid clientId
+ *
+ * @return void
+ */
+ public function deleteBasicToken($key)
+ {
+ $accessTokenService = new BasicAuthService($this->getContainer());
+ $accessTokenService->deleteToken($key);
+ }
+
+ /**
+ * Add a new basic Token
+ * @param string $name
+ * @param string $description
+ * @param int $expiresAt Unix timestamp
+ * @param string $user user id
+ * @param array $selectedScopes scope names
+ * @param string $key
+ * @param string $secret
+ *
+ * @return \API\Document\AccessToken
+ */
+ public function addToken($name, $description, $expiresAt, $user, $selectedScopes, $key = null, $secret = null)
+ {
+ $basicAuthService = new BasicAuthService($this->getContainer());
+ $token = $basicAuthService->addToken($name, $description, $expiresAt, $user, $selectedScopes, $key, $secret);
+ return $token;
+ }
+}
diff --git a/src/xAPI/Admin/LrsReport.php b/src/xAPI/Admin/LrsReport.php
new file mode 100644
index 00000000..2b1adfa2
--- /dev/null
+++ b/src/xAPI/Admin/LrsReport.php
@@ -0,0 +1,473 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Admin;
+
+use API\Bootstrap;
+use API\Config;
+use API\Container;
+use API\Service\Auth as Auth;
+use API\Storage\Adapter\Mongo as Mongo;
+
+/**
+ * LRS Status Report
+ */
+class LrsReport
+{
+ private $reports = [];
+
+ private $count = [
+ 'total' => 0,
+ 'completed' => 0,
+ ];
+
+ /**
+ * @constructor
+ */
+ public function __construct()
+ {
+ if (!Bootstrap::mode()) {
+ $bootstrap = Bootstrap::factory(Bootstrap::Config);
+ }
+ }
+
+ ////
+ // Report building
+ ////
+
+ /**
+ * Run comprehensive LRS check
+ *
+ * @return array report
+ */
+ public function check()
+ {
+ $ok = $this->checkConfigYml();
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkConfigYml();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkMongo();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkMongoversion();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkDataBase();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkUsersAndPermissions();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkXapiDocumentStats();
+ }
+ $this->count($ok);
+
+ if ($ok) {
+ $ok = $this->checkLocalFileStorage();
+ }
+ $this->count($ok);
+
+ return $this->reports;
+ }
+
+ /**
+ * Compute a summary of performed checks
+ *
+ * @return array summary
+ */
+ public function summary()
+ {
+ $summary = array_merge($this->count, [
+ 'reports' => [
+ 'success' => 0,
+ 'error' => 0,
+ 'warn' => 0,
+ 'total' => 0,
+ ]
+ ]);
+
+ foreach ($this->reports as $section => $report) {
+ foreach ($report as $label => $item) {
+ switch ($item['status']) {
+ case 'success': {
+ $summary['reports']['success']++;
+ break;
+ }
+ case 'error': {
+ $summary['reports']['error']++;
+ break;
+ }
+ case 'warn': {
+ $summary['reports']['warn']++;
+ break;
+ }
+ }
+ $summary['reports']['total']++;
+ }
+ }
+
+ return $summary;
+ }
+
+ /**
+ * count a report result
+ * @param bool $ok
+ *
+ * @return void
+ */
+ private function count($ok)
+ {
+ $this->count['total']++;
+ $this->count['completed'] += (int) $ok;
+ }
+
+ ////
+ // Checks
+ ////
+
+ /**
+ * Run basic checks on configuration yml files
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkConfigYml()
+ {
+ $setup = new Setup();
+
+ $config = [];
+ try {
+ $config = $setup->loadYaml('Config.yml');
+ } catch (AdminException $e) {
+ $this->error('Config', 'Config.yml', $e->getMessage());
+ return false;
+ }
+ $this->success('Config', 'Config.yml');
+
+ $data = [];
+ try {
+ $data = $setup->loadYaml('Config.production.yml');
+ $this->success('Config', 'Config.production.yml');
+ } catch (AdminException $e) {
+ $this->error('Config', 'Config.production.yml', $e->getMessage());
+ }
+
+ $data = [];
+ try {
+ $data = $setup->loadYaml('Config.development.yml');
+ $this->success('Config', 'Config.development.yml');
+ } catch (AdminException $e) {
+ $this->warn('Config', 'Config.development.yml', $e->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Run basic checks on Mongo connection
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkMongo()
+ {
+ $setup = new Setup();
+
+ $conn = Config::get(['storage', 'Mongo', 'host_uri']);
+ $buildInfo = $setup->testDbConnection($conn);
+
+ if (false === $buildInfo) {
+ $this->error('Mongo', 'connection', $conn.' not a valid Mongo connection');
+ return false;
+ } else {
+ $this->success('Mongo', 'connection', $buildInfo->version);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check Mongo Database version requirements
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkMongoversion()
+ {
+ $setup = new Setup();
+
+ try {
+ $msg = $setup->verifyDbVersion();
+ $this->success('Mongo', 'compatibility', $msg);
+ } catch (AdminException $e) {
+ $this->error('Mongo', 'compatibility', $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Run basic checks/stats on Mongo DB
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkDataBase()
+ {
+ $mongo = new Mongo(new Container());
+ $cursor = $mongo->executeCommand(['dbStats' => 1, 'scale' => 1024 * 1024]);
+ $result = $cursor->toArray()[0];
+
+ $this->notice('Mongo', 'database', $result->db);
+
+ $this->notice('Mongo', 'collections', $this->numberFormat($result->collections));
+ $this->notice('Mongo', 'objects', $this->numberFormat($result->objects));
+ $this->notice('Mongo', 'dataSize', $this->numberFormat($result->dataSize, 'Mb'));
+ $this->notice('Mongo', 'storageSize', $this->numberFormat($result->storageSize, 'Mb'));
+ $this->notice('Mongo', 'fileSize', $this->numberFormat($result->storageSize, 'Mb'));
+
+ return true;
+ }
+
+ /**
+ * Run basic checks and stats on stored permissions, users, and tokens
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkUsersAndPermissions()
+ {
+ $mongo = new Mongo(new Container());
+ $auth = new Auth(new Container());
+
+ $count = count(array_keys($auth->getAuthScopes()));
+ if (!$count) {
+ $this->error('Collections', 'authScopes', 'No Authentication Scopes', 'LRS setup is incomplete');
+ } else {
+ $this->success('Collections', 'authScopes', $count);
+ }
+
+ $count = $mongo->count(Mongo\User::COLLECTION_NAME);
+ if (!$count) {
+ $this->error('Collections', 'users', 'No users', 'LRS is not accessible');
+ } else {
+ $this->success('Collections', 'users', $count);
+ }
+
+ $count = $mongo->count(Mongo\BasicAuth::COLLECTION_NAME);
+ if (!$count) {
+ $this->warn('Collections', 'basicTokens', 'No basic tokens', 'LRS is not accessible via HTTP basic');
+ } else {
+ $this->success('Collections', 'basicTokens', $count);
+ }
+
+ $count = $mongo->count(Mongo\OAuthClients::COLLECTION_NAME);
+ if (!$count) {
+ $this->warn('Collections', 'oAuthClients', 'No oAuth clients', 'LRS is not accessible via oAuth');
+ } else {
+ $this->success('Collections', 'oAuthClients', $count);
+ }
+
+ $count = $mongo->count(Mongo\OAuth::COLLECTION_NAME);
+ $this->notice('Collections', 'oAuthTokens', $count);
+
+ return true;
+ }
+
+ /**
+ * Return counts for xapi document storage
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkXapiDocumentStats()
+ {
+ $mongo = new Mongo(new Container());
+
+ $count = $mongo->count(Mongo\Statement::COLLECTION_NAME);
+ $this->info('xAPI Documents', 'Statements', $count);
+
+ $count = $mongo->count(Mongo\Activity::COLLECTION_NAME);
+ $this->info('xAPI Documents', 'Activities', $count);
+
+ $count = $mongo->count(Mongo\ActivityProfile::COLLECTION_NAME);
+ $this->info('xAPI Documents', 'Activity Profiles', $count);
+
+ $count = $mongo->count(Mongo\ActivityState::COLLECTION_NAME);
+ $this->info('xAPI Documents', 'Activity States', $count);
+
+ $count = $mongo->count(Mongo\Attachment::COLLECTION_NAME);
+ $this->info('xAPI Documents', 'Activity States', $count);
+
+ return true;
+ }
+
+ /**
+ * Run basic checks and stats on local file storage
+ *
+ * @return bool indicator if tests were completed
+ */
+ private function checkLocalFileStorage()
+ {
+ $root = Config::get(['publicRoot'], ''.time());
+ $dir = Config::get(['filesystem', 'local', 'root_dir'], ''.time());
+ $path = $root.'/'.$dir;
+
+ $abspath = realpath($path);
+
+ if (false == $abspath) {
+ $this->error('FileStorage', 'local', 'directory not found or not readable', $path);
+ return false;
+ }
+
+ $size = $this->dirSize($abspath);
+ $this->success('FileStorage', 'local', $this->numberFormat($size/ (1024 * 1024), 'Mb'), $abspath);
+ return true;
+ }
+
+ ////
+ // Notifications
+ ////
+
+ /**
+ * Register a report
+ * @param string $section section
+ * @param string $label section label
+ * @param string $status [success, error, warn, notice]
+ * @param string $value message
+ * @param string $note
+ *
+ * @return void
+ */
+ private function set($section, $label, $status, $value, $note = '')
+ {
+ if (!isset($this->reports[$section])) {
+ $this->reports[$section] = [];
+ }
+ if (!isset($this->reports[$section][$label])) {
+ $this->reports[$section][$label] = [];
+ }
+
+ $this->reports[$section][$label] = [
+ 'status' => $status,
+ 'value' => $value,
+ 'note' => $note,
+ ];
+ }
+
+ /**
+ * Add notification of serverity 'info'
+ *
+ * @return void
+ */
+ private function info($section, $label, $value, $note = '')
+ {
+ $this->set($section, $label, 'info', $value, $note);
+ }
+
+ /**
+ * Add notification of serverity 'notice'
+ *
+ * @return void
+ */
+ private function notice($section, $label, $value, $note = '')
+ {
+ $this->set($section, $label, 'notice', $value, $note);
+ }
+
+ /**
+ * Add notification of serverity 'success'
+ *
+ * @return void
+ */
+ private function success($section, $label, $value = 'ok', $note = '')
+ {
+ $this->set($section, $label, 'success', $value, $note);
+ }
+
+ /**
+ * Add notification of serverity 'warn'
+ *
+ * @return void
+ */
+ private function warn($section, $label, $value, $note = '')
+ {
+ $this->set($section, $label, 'warn', $value, $note);
+ }
+
+ /**
+ * Add notification of serverity 'error'
+ *
+ * @return void
+ */
+ private function error($section, $label, $value, $note = '')
+ {
+ $this->set($section, $label, 'error', $value, $note);
+ }
+
+ ////
+ // Helpers
+ ////
+
+ /**
+ * Formats a float number (english notation, 2 decimals)
+ * @param mixed $val
+ * @param string $unit suffix
+ *
+ * @return string
+ */
+ public function numberFormat($val, $unit = null)
+ {
+ $unit = ($unit) ? ' '.$unit : '';
+ return number_format((float)$val, 2, '.', '').$unit;
+ }
+
+ /**
+ * Compute directory size recursively
+ *
+ * @return int bytes
+ */
+ public function dirSize($path)
+ {
+ $total = 0;
+ $path = realpath($path);
+ if ($path !== false && $path != '' && file_exists($path)) {
+ foreach (
+ new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
+ ) as $object) {
+ $total += $object->getSize();
+ }
+ }
+ return $total;
+ }
+}
diff --git a/src/xAPI/Admin/Setup.php b/src/xAPI/Admin/Setup.php
new file mode 100644
index 00000000..d1eb7285
--- /dev/null
+++ b/src/xAPI/Admin/Setup.php
@@ -0,0 +1,290 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Admin;
+
+use Symfony\Component\Yaml\Yaml;
+
+use API\Config;
+use API\Bootstrap;
+use API\Storage\Adapter\Mongo as Mongo;
+use API\Service\Auth\OAuth as OAuthService;
+
+use API\Storage\AdapterException;
+use MongoDB\Driver\Exception\Exception as MongoException;
+
+/**
+ * Scratch-Api for various Admin tasks who are not dependend on a bootstrapped application
+ */
+class Setup
+{
+ /**
+ * @var string configDir path to config folder
+ */
+ private $configDir;
+
+ /**
+ * @var string yamlData internal cache for config.yaml data
+ */
+ private $yamlData;
+
+ /**
+ * constructor
+ */
+ public function __construct()
+ {
+ if (!Bootstrap::mode()) {
+ $bootstrap = Bootstrap::factory(Bootstrap::Config);
+ }
+ $this->configDir = Config::get('appRoot').'/src/xAPI/Config/';
+ }
+
+ /**
+ * Checks if a yaml config file exists already in /src/xAPI/Config/.
+ * @param string $configYML yaml file
+ *
+ * @return string|false
+ */
+ public function locateYaml($yml)
+ {
+ clearstatcache();
+ return realpath($this->configDir.'/'.$yml);
+ }
+
+ /**
+ * Loads a config yml file in /src/xAPI/Config/.
+ * @param string $yaml yaml file to be created from template
+ * @returns array $data associative array of parsed data
+ *
+ * @return array $data
+ * @throws AdminException
+ */
+ public function loadYaml($yml)
+ {
+ $file = $this->locateYaml($yml);
+ if (false === $file) {
+ throw new AdminException('File `'.$yml.'` not found.');
+ }
+
+ $contents = file_get_contents($file);
+ if (false === $contents) {
+ throw new AdminException('Error reading file `'.$yml.'`. Make sure the file exists and is readable.');
+ }
+
+ try {
+ $data = Yaml::parse($contents, true);
+ } catch (\Exception $e) {
+ // @see \Symfony\Component\Yaml\Yaml::parse()
+ throw new AdminException('Error parsing data from file `'.$yml.'`');
+ }
+
+ if (!$data) {
+ throw new AdminException('Error parsing data from file `'.$yml.'`: Empty data.');
+ }
+
+ return $data;
+ }
+
+ /**
+ * Deletes a yaml file. Throws an Exception on failure
+ * @param string $yml yaml file to be created from template
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function removeYaml($yml)
+ {
+ if (!$this->locateYaml($yml)) {
+ return;
+ }
+
+ $file = $this->configDir.'/'.$yml;
+ if (false === unlink($file)) {
+ throw new AdminException('Error deleting `'.$file.'`. Make sure the directory is writable.');
+ }
+ }
+
+ /**
+ * Creates a config yml file in /src/xAPI/Config/ from an existing template, merges data with template data.
+ * @param string $yml yaml file to be created from template
+ * @param array $mergeData associative array of config data to be merged in to the new config file
+ *
+ * @return array $data
+ * @throws AdminException
+ */
+ public function installYaml($yml, array $mergeData = [])
+ {
+ if ($this->locateYaml($yml)) {
+ throw new AdminException('File `'.$yml.'` exists already. The LRS configuration would be overwritten. To restore the defaults you must manually remove the file first.');
+ }
+
+ $data = $this->loadYaml('Templates/'.$yml);
+ if (!empty($mergeData)) {
+ $data = array_merge($data, $mergeData);
+ }
+
+ $file = $this->configDir.'/'.$yml;
+ $contents = Yaml::dump($data, 3, 4);// exceptionOnInvalidType
+ if (false === file_put_contents($file, $contents)) {
+ throw new AdminException('Error writing file `'.$file.'` Make sure the directory is writable.');
+ }
+
+ return $data;
+ }
+
+ /**
+ * creates a config yml file in /src/xAPI/Config/ from an existing template, merges data with template data.
+ * @param string $yaml yaml file to be created from template
+ * @param array $update associative array of config data to be merged in to the new config file
+ *
+ * @return array $data
+ * @throws AdminException
+ */
+ public function updateYaml($yml, array $update)
+ {
+ $file = $this->locateYaml($yml);
+ $data = $this->loadYaml($yml);
+
+ $data = array_merge($data, $update);
+ $contents = Yaml::dump($data, 3, 4);// exceptionOnInvalidType
+ if (false === file_put_contents($file, $contents)) {
+ throw new AdminException('Error updating file `'.$file.'` Make sure the directory is writable.');
+ }
+ return $data;
+ }
+
+ /**
+ * Test Mongo DB access
+ * @param string $uri connection uri
+ *
+ * @return stdClass|false connection result (mongo.buildInfo)
+ */
+ public function testDbConnection($uri)
+ {
+ // \MongoDB\Driver\Manager will use mongodb://127.0.0.1/ when no or empty uri was submitted
+ if (!$uri) {
+ return false;
+ }
+
+ try {
+ $buildInfo = Mongo::testConnection($uri);
+ return $buildInfo;
+ } catch (\MongoDB\Driver\Exception\InvalidArgumentException $e) {
+ return false;
+ } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Test Mongo DB access
+ * @param string $uri connection uri
+ *
+ * @return string version info if versions are compatible
+ * @throws AdminException if installed version is lower than required
+ */
+ public function verifyDbVersion($container = null)
+ {
+ if (!$container) {
+ $container = Bootstrap::getContainer();
+ }
+ $mongo = new Mongo($container);
+
+ try {
+ $info = $mongo->verifyDatabaseVersion();
+ } catch (AdapterException $e) {
+ throw new AdminException($e->getMessage());
+ }
+ return sprintf('Available: "%s", Required: "%s"', $info['installed'], $info['required']);
+ }
+
+ /**
+ * Install Database
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function installDb($container = null)
+ {
+ if (!$container) {
+ $container = Bootstrap::getContainer();
+ }
+
+ $schema = new Mongo\Schema($container);
+
+ try {
+ $schema->install();
+ } catch (MongoException $e) {
+ throw new AdminException('Error installing Database. Error: '. $e->getMessage());
+ } catch (AdapterException $e) {
+ throw new AdminException('Error installing Database. Error: '. $e->getMessage());
+ }
+ }
+
+
+ /**
+ * Creates writable storage directories for files (attachments) and logs
+ *
+ * @return \SplFileInfo
+ * @throws \Exception
+ */
+ public function installFileStorage()
+ {
+ $root = realpath(Config::get('appRoot'));
+ if (!$root) {
+ throw new AdminException('Error installing local FS: Missing Config[appRoot]');
+ }
+
+ $dir = $root.'/storage';
+ if (false === $this->createStorageDir($dir)) {
+ throw new AdminException('Unable to create folder: '.$dir);
+ }
+
+ $dir = $root.'/storage/files';
+ if (false === $this->createStorageDir($dir)) {
+ throw new AdminException('Unable to create folder: '.$dir);
+ }
+
+ $dir = $root.'/storage/logs';
+ if (false === $this->createStorageDir($dir)) {
+ throw new AdminException('Unable to create folder: '.$dir);
+ }
+
+ return new \SplFileInfo($root.'/storage');
+ }
+
+ /**
+ * Creates Storage dir with approbiate permissions
+ * @param string $dir (real) path to dir to create
+ *
+ * @return bool
+ */
+ private function createStorageDir($dir)
+ {
+ if (is_dir($dir)) {
+ return true;
+ }
+ return @mkdir($dir, 0755);// surpress PHP warning in console output
+ }
+}
diff --git a/src/xAPI/Admin/User.php b/src/xAPI/Admin/User.php
new file mode 100644
index 00000000..37cf1ecb
--- /dev/null
+++ b/src/xAPI/Admin/User.php
@@ -0,0 +1,126 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Admin;
+
+use API\Service\User as UserService;
+use API\Service\Auth as AuthService;
+use API\Admin;
+
+/**
+ * User managment
+ */
+class User extends Admin
+{
+ /**
+ * Fetch all permissions from Mongo
+ * @return array collection of permissions with their name as key
+ */
+ public function fetchAvailablePermissions()
+ {
+ $service = new AuthService($this->getContainer());
+ return $service->getAuthScopes();
+ }
+
+ /**
+ * Fetch all permissions from Mongo
+ * @return array collection of permissions with their name as key
+ */
+ public function mergeInheritedPermissions($names)
+ {
+ $service = new AuthService($this->getContainer());
+ return $service->mergeInheritance($names);
+ }
+
+ /**
+ * Add a user record
+ * @param string $email
+ * @param string $password
+ * @param array $selectedPermissions selected scope permission records
+ * @return stdClass Mongo user record
+ * @throws \API\RuntimeException
+ */
+ public function addUser($name, $description, $email, $password, $permissions)
+ {
+ $v = new Validator();
+ $v->validateName($name);
+ $v->validatePassword($password);
+
+ $this->validateUserEmail($email);
+
+ // fetch available permissions and compare
+ $service = new UserService($this->getContainer());
+ $user = $service->addUser($name, $description, $email, $password, $permissions);
+
+ return $user;
+ }
+
+ /**
+ * Get user for objectId
+ * @param \MongoDB\BSON\ObjectID $objectId
+ * @return stdClass|null
+ */
+ public function getUser(\MongoDB\BSON\ObjectID $objectId)
+ {
+ $service = new UserService($this->getContainer());
+ return $service->findById($objectId);
+ }
+
+ /**
+ * Fetch all user email addresses
+ *
+ * @return array collection of user records with email as key
+ */
+ public function fetchAllUserEmails()
+ {
+ // // TODO 0.11.x paginated query
+ $userService = new UserService($this->getContainer());
+ $documentResult = $userService->fetchAll();
+ $users = [];
+ foreach ($documentResult->getCursor() as $user) {
+ $users[$user->email] = $user;
+ }
+
+ return $users;
+ }
+
+ /**
+ * Validate email address by format and uniqueness
+ * - validate format
+ * - check if email already exists in users
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateUserEmail($email)
+ {
+ $v = new Validator();
+ $v->validateEmail($email);
+
+ $uservice = new UserService($this->getContainer());
+ if ($uservice->hasEmail($email)) {
+ throw new AdminException('User email exists already');
+ }
+ }
+}
diff --git a/src/xAPI/Admin/Validator.php b/src/xAPI/Admin/Validator.php
new file mode 100644
index 00000000..de0bcf87
--- /dev/null
+++ b/src/xAPI/Admin/Validator.php
@@ -0,0 +1,196 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Admin;
+
+use API\Bootstrap;
+use API\Config;
+
+/**
+ * Admin validator functions
+ */
+class Validator
+{
+
+ /**
+ * constructor
+ */
+ public function __construct()
+ {
+ if (!Bootstrap::mode()) {
+ $bootstrap = Bootstrap::factory(Bootstrap::Config);
+ }
+ }
+
+ /**
+ * Validate name param
+ * @param string $str
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateName(string $str)
+ {
+ $errors = [];
+ $length = 4;
+
+ if (!$str || strlen($str) < $length) {
+ $errors[] = 'Must have at least '.$length.' characters';
+ }
+
+ if (!empty($errors)) {
+ throw new AdminException(json_encode($errors));
+ }
+ }
+
+ /**
+ * Validate password
+ * @param string $str
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validatePassword(string $str)
+ {
+ $errors = [];
+ $length = 8;
+
+ if (strlen($str) < $length) {
+ $errors[] = 'Must have at least '.$length.' characters';
+ }
+
+ if (!preg_match('/[0-9]+/', $str)) {
+ $errors[] = 'Must include at least one number.';
+ }
+
+ if (!preg_match('/[a-zA-Z]+/', $str)) {
+ $errors[] = 'Must include at least one letter.';
+ }
+
+ if (!preg_match('/[A-Z]+/', $str)) {
+ $errors[] = 'Must include at least one CAPS!';
+ }
+
+ if (!preg_match('/\W+/', $str)) {
+ $errors[] = 'Must include at least one symbol!';
+ }
+
+ if (!empty($errors)) {
+ throw new AdminException(json_encode($errors));
+ }
+ }
+
+ /**
+ * Validate email address
+ * @param string $email
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateEmail(string $email)
+ {
+ if (!filter_var($email, \FILTER_VALIDATE_EMAIL)) {
+ throw new AdminException('Invalid email address!');
+ }
+ }
+
+ /**
+ * Validate xAPI permission scopes
+ * @param array $perms array of strings: permissions to check
+ * @param array $available array of strings: available permissions to check against
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateXapiPermissions(array $perms, array $available)
+ {
+ if (empty($perms)) {
+ throw new AdminException('Permissions cannot be empty.');
+ }
+
+ if (empty($available)) {
+ throw new AdminException('Available permissions cannot be empty.');
+ }
+
+ $aNames = array_keys($available);
+
+ foreach ($perms as $name) {
+ if (!in_array($name, $aNames)) {
+ throw new AdminException('Invalid permission');
+ }
+ }
+ }
+
+ /**
+ * Validate an absolute url, require at least scheme and host
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateRedirectUri(string $str)
+ {
+ $components = parse_url($str);
+ if (false === $components) {
+ throw new AdminException('Invalid url');
+ }
+
+ if (!isset($components['scheme'])) {
+ throw new AdminException('Redirect url requires a valid scheme');
+ }
+
+ if (!isset($components['host'])) {
+ throw new AdminException('Redirect url requires a valid host component');
+ }
+ }
+
+ /**
+ * Validate Mongo Naming (database and collection name)
+ * @see https://docs.mongodb.com/manual/reference/limits/
+ *
+ * @return void
+ * @throws AdminException
+ */
+ public function validateMongoName(string $str)
+ {
+ $errors = [];
+ $minLength = 4;// mongo does only require a length > 0
+ $maxLength = 64;
+
+ if (!$str || strlen($str) < $minLength) {
+ $errors[] = 'Must have at least '.$minLength.' characters';
+ }
+
+ if (!$str || strlen($str) > $maxLength) {
+ $errors[] = 'Must less than '.$maxLength.' characters';
+ }
+
+ if (!preg_match('/^[a-z0-9_\-]+$/i', $str)) {
+ $errors[] = 'Can only contain letter, numbers, dashes and underscores';
+ }
+
+ if (!empty($errors)) {
+ throw new AdminException(json_encode($errors));
+ }
+ }
+}
diff --git a/src/xAPI/Document/ActivityState.php b/src/xAPI/AppInitException.php
similarity index 79%
rename from src/xAPI/Document/ActivityState.php
rename to src/xAPI/AppInitException.php
index ec2445fc..dfaf500b 100644
--- a/src/xAPI/Document/ActivityState.php
+++ b/src/xAPI/AppInitException.php
@@ -1,9 +1,8 @@
getStateId();
- }
}
diff --git a/src/xAPI/BaseTrait.php b/src/xAPI/BaseTrait.php
new file mode 100644
index 00000000..2686587d
--- /dev/null
+++ b/src/xAPI/BaseTrait.php
@@ -0,0 +1,90 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+use Interop\Container\ContainerInterface;
+
+trait BaseTrait
+{
+ /**
+ * @var ContainerInterface $container Holds service container
+ */
+ private $container;
+
+ /**
+ * Sets service container.
+ *
+ * @param ContainerInterface $container
+ * @return self
+ */
+ public function setContainer(ContainerInterface $container)
+ {
+ $this->container = $container;
+ return $this;
+ }
+
+ /**
+ * Gets service ontainer.
+ *
+ * @return ContainerInterface
+ */
+ public function getContainer()
+ {
+ if (!$this->container) {
+ throw new \Exception('Basetrait: no container was set.');
+ }
+ return $this->container;
+ }
+
+ /**
+ * Gets storage service.
+ *
+ * @return \Storage\AdapterInterface
+ * @throws \Exception
+ * @throws \API\ContainerException
+ */
+ public function getStorage()
+ {
+ if (!$this->container) {
+ throw new \Exception('Basetrait: no container was set.');
+ }
+ return $this->container->get('storage');
+ }
+
+ /**
+ * Gets log service.
+ *
+ * @return \Monolog\Monolog
+ * @throws \Exception
+ * @throws \API\ContainerException
+ */
+ public function getLog()
+ {
+ if (!$this->container) {
+ throw new \Exception('Basetrait: no container was set.');
+ }
+ return $this->container->get('log');
+ }
+}
diff --git a/src/xAPI/Bootstrap.php b/src/xAPI/Bootstrap.php
new file mode 100644
index 00000000..5d723dfb
--- /dev/null
+++ b/src/xAPI/Bootstrap.php
@@ -0,0 +1,654 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+use Monolog\Logger;
+use Symfony\Component\Yaml\Parser as YamlParser;
+use API\Controller;
+use League\Url\Url;
+use API\Util\Collection;
+use API\Service\Auth\OAuth as OAuthService;
+use API\Service\Auth\Basic as BasicAuthService;
+use API\Service\Log as LogService;
+use API\Service\Auth as AuthService;
+use API\Parser\PsrRequest as PsrRequestParser;
+use API\Service\Auth\Exception as AuthFailureException;
+use API\Util\Versioning;
+use Slim\DefaultServicesProvider;
+use Slim\App as SlimApp;
+use API\Controller\Error;
+use API\Config;
+use API\Console\Application as CliApp;
+
+/**
+ * Bootstrap lxHive
+ *
+ * Bootstrap routines fall into two steps:
+ * 1 factory (initialization)
+ * 2 boot application
+ * Example for booting a Web App:
+ * $bootstrap = \API\Bootstrap::factory(\API\Bootstrap::Web);
+ * $app = $bootstrap->bootWebApp();
+ * An app can only be bootstraped once, the only Exception being mode Bootstap::Testing
+ */
+class Bootstrap
+{
+ /**
+ * @var string VERSION phpversion() parable application version string(SemVer), synchs with Config.yml version
+ */
+ const VERSION = '0.10.0';
+
+ /**
+ * @var Bootstrap Mode
+ */
+ const None = 0;
+ const Web = 1;
+ const Console = 2;
+ const Testing = 3;
+ const Config = 4;
+
+ private static $containerInstance = null;
+ private static $containerInstantiated = false;
+
+ private static $mode = 0;
+
+ /**
+ * constructor
+ * Sets bootstrap mode
+ * @param int $mode Bootstrap mode constant
+ */
+ private function __construct($mode)
+ {
+ self::$mode = ($mode) ? $mode : self::None; // casting [0, null, false] to self::None
+ }
+
+ /**
+ * Factory for container contained within bootstrap, which is a base for various initializations
+ *
+ * | Mode | config | services | routes | extensions | can reboot? | scope |
+ * |--------------------|----------|-----------|-----------|------------|---------------| ----------------------|
+ * | Bootstrap::None | - | - | - | - | yes | n/a |
+ * | Bootstrap::Config | x | - | - | - | yes | load config only |
+ * | Bootstrap::Testing | x | x | - | - | yes | unit tests |
+ * | Bootstrap::Console | x | x | - | - | no | admin console |
+ * | Bootstrap::Web | x | x | x | x | no | default: run web app |
+ *
+ * @param int $mode Bootstrap mode constant
+ * @return void
+ * @throw AppInitException
+ */
+ public static function factory($mode)
+ {
+ if (self::$containerInstantiated) {
+ // modes test and none (admin,etc) shall pass
+ if (
+ $mode !== self::Testing
+ && $mode !== self::None
+ && $mode !== self::Config
+ ) {
+ throw new AppInitException('Bootstrap: You can only instantiate the Bootstrapper once!');
+ }
+ }
+
+ $bootstrap = new self($mode);
+
+ switch (self::$mode) {
+ case self::Web: {
+ $bootstrap->initConfig();
+ $container = $bootstrap->initWebContainer();
+ self::$containerInstance = $container;
+ self::$containerInstantiated = true;
+ return $bootstrap;
+ break;
+ }
+
+ case self::Console: {
+ $bootstrap->initConfig();
+ $container = $bootstrap->initCliContainer();
+ self::$containerInstance = $container;
+ self::$containerInstantiated = true;
+ return $bootstrap;
+ break;
+ }
+
+ case self::Testing: {
+ $bootstrap->initConfig();
+ $container = $bootstrap->initGenericContainer();
+ self::$containerInstance = $container;
+ self::$containerInstantiated = true;
+ return $bootstrap;
+ break;
+ }
+
+ case self::Config: {
+ $bootstrap->initConfig();
+ return $bootstrap;
+ break;
+ }
+
+ case self::None: {
+ return $bootstrap;
+ break;
+ }
+
+ default: {
+ throw new AppInitException('Bootstrap: You must provide a valid mode when calling the Boostrapper factory!');
+ }
+ }
+ }
+
+ /**
+ * Reset Bootstrap
+ * @ignore do not compile to docs
+ * @return void
+ * @throw AppInitException if self::mode does not allow reboot
+ */
+ public static function reset()
+ {
+ if (
+ self::$mode === self::Testing
+ || self::$mode === self::None
+ || self::$mode === self::Config
+ ) {
+ self::$mode = self::None;
+ self::$containerInstantiated = false;
+ self::$containerInstance = false;
+ Config::reset();
+ return;
+ }
+
+ throw new AppInitException('Bootstrap: reset not allowed in this mode (' . self::$mode . ')');
+ }
+
+ /**
+ * Returns the current bootstrap mode
+ * See mode constants.
+ * Check if the Bootstrap was initialized
+ * if(!Bootstrap::mode()) {
+ * ...
+ * }
+ * @return int current mode
+ */
+ public static function mode()
+ {
+ return self::$mode;
+ }
+
+ /**
+ * Get service container
+ * @return \Interop\Container\ContainerInterface|null
+ */
+ public static function getContainer()
+ {
+ return self::$containerInstance;
+ }
+
+ /**
+ * Initialize default configuration and load services
+ * @return \Psr\Container\ContainerInterface service container
+ * @throws AppInitException if self::$mode > self::None
+ */
+ public function initConfig()
+ {
+ // Defaults
+ $appRoot = realpath(__DIR__.'/../../');
+ $defaults = [
+ 'appRoot' => $appRoot,
+ 'publicRoot' => $appRoot.'/public',
+ ];
+
+ Config::factory($defaults);
+
+ $filesystem = new \League\Flysystem\Filesystem(new \League\Flysystem\Adapter\Local($appRoot));
+ $yamlParser = new YamlParser();
+
+ try {
+ $contents = $filesystem->read('src/xAPI/Config/Config.yml');
+ $config = $yamlParser->parse($contents);
+ } catch (\League\Flysystem\FileNotFoundException $e) {
+ if (self::$mode === self::None) {
+ return;
+ } else {
+ // throw AppInit exception
+ throw new AppInitException('Cannot load configuration: '.$e->getMessage());
+ }
+ }
+
+ try {
+ $contents = $filesystem->read('src/xAPI/Config/Config.' . $config['mode'] . '.yml');
+ $config = array_merge($config, $yamlParser->parse($contents));
+ } catch (\League\Flysystem\FileNotFoundException $e) {
+ // Ignore exception
+ }
+
+ // ad-hoc db for unit test @see phpunit.xml
+ if (defined('LXHIVE_UNITTEST')) {
+ $config['storage']['Mongo']['db_name'] = 'LXHIVE_UNITTEST';
+ }
+
+ Config::merge($config);
+ }
+
+ /**
+ * Initialize default configuration and load services
+ * @return \Psr\Container\ContainerInterface service container
+ * @throws AppInitException
+ */
+ public function initGenericContainer()
+ {
+ // 2. Create default container
+ if (self::$mode === self::Web) {
+ $container = new \Slim\Container();
+ } else {
+ $container = new Container();
+ }
+
+ // 3. Storage setup
+ $container['storage'] = function ($container) {
+ $storageInUse = Config::get(['storage', 'in_use']);
+ $storageClass = '\\API\\Storage\\Adapter\\'.$storageInUse;
+ if (!class_exists($storageClass)) {
+ throw new AppInitException('Bootstrap: Storage type selected in config is invalid!');
+ }
+ $storageAdapter = new $storageClass($container);
+
+ return $storageAdapter;
+ };
+
+ return $container;
+ }
+
+ /**
+ * Initialize web mode configuration and load services
+ * @return \Psr\Container\ContainerInterface service container
+ * @throws AppInitException
+ * @throws HttpException on authentication denied or invalid request
+ */
+ public function initWebContainer($container = null)
+ {
+ $appRoot = realpath(__DIR__.'/../../');
+ $container = $this->initGenericContainer($container);
+
+ // 4. Set up Slim services
+ /*
+ * Slim\App expects a container that implements Interop\Container\ContainerInterface
+ * with these service keys configured and ready for use:
+ *
+ * - settings: an array or instance of \ArrayAccess
+ * - environment: an instance of \Slim\Interfaces\Http\EnvironmentInterface
+ * - request: an instance of \Psr\Http\Message\ServerRequestInterface
+ * - response: an instance of \Psr\Http\Message\ResponseInterface
+ * - router: an instance of \Slim\Interfaces\RouterInterface
+ * - foundHandler: an instance of \Slim\Interfaces\InvocationStrategyInterface
+ * - errorHandler: a callable with the signature: function($request, $response, $exception)
+ * - notFoundHandler: a callable with the signature: function($request, $response)
+ * - notAllowedHandler: a callable with the signature: function($request, $response, $allowedHttpMethods)
+ * - callableResolver: an instance of \Slim\Interfaces\CallableResolverInterface
+ */
+ $slimDefaultServiceProvider = new DefaultServicesProvider();
+ $slimDefaultServiceProvider->register($container);
+
+ // 5. Insert URL object
+ // TODO 0.11.x: Remove this soon - use PSR-7 request's URI object
+ // TODO 0.10.x: Handle better rather than supressing exceptions when running Unit tests (i.e., create mock ServerEnvironment)
+ try {
+ $container['url'] = Url::createFromServer($_SERVER);
+ } catch (\RuntimeException $e) {
+ // See comment above
+ }
+
+ $handlerConfig = Config::get(['log', 'handlers']);
+ $stream = $appRoot.'/storage/logs/' . Config::get('mode') . '.' . date('Y-m-d') . '.log';
+
+ if (null === $handlerConfig) {
+ $handlerConfig = ['ErrorLogHandler'];
+ }
+
+ $logger = new Logger('web');
+
+ $formatter = new \Monolog\Formatter\LineFormatter();
+
+ // Set up logging
+ if (in_array('FirePHPHandler', $handlerConfig)) {
+ $handler = new \Monolog\Handler\FirePHPHandler();
+ $logger->pushHandler($handler);
+ }
+
+ if (in_array('ChromePHPHandler', $handlerConfig)) {
+ $handler = new \Monolog\Handler\ChromePHPHandler();
+ $logger->pushHandler($handler);
+ }
+
+ if (in_array('StreamHandler', $handlerConfig)) {
+ $handler = new \Monolog\Handler\StreamHandler($stream);
+ $handler->setFormatter($formatter);
+ $logger->pushHandler($handler);
+ }
+
+ if (in_array('ErrorLogHandler', $handlerConfig)) {
+ $handler = new \Monolog\Handler\ErrorLogHandler();
+ $handler->setFormatter($formatter);
+ $logger->pushHandler($handler);
+ }
+
+ $container['logger'] = $logger;
+
+ $container['errorHandler'] = function ($container) {
+ return function ($request, $response, $exception) use ($container) {
+ $data = [];
+ $code = $exception->getCode();
+ if ($code < 100) {
+ $code = 500;
+ }
+ if (method_exists($exception, 'getData')) {
+ $data = $exception->getData();
+ }
+ $errorResource = new Error($container, $request, $response);
+ $error = $errorResource->error($code, $exception->getMessage(), $data);
+
+ return $error;
+ //return $c['response']->withStatus($code)
+ // ->withHeader('Content-Type', 'application/json')
+ // ->write(json_encode([$e->getMessage(), $data]));
+ };
+ };
+
+ $container['eventDispatcher'] = new \Symfony\Component\EventDispatcher\EventDispatcher();
+
+ // Parser
+ $container['parser'] = function ($container) {
+ $parser = new PsrRequestParser($container['request']);
+
+ return $parser;
+ };
+
+ // Request logging
+ $container['requestLog'] = function ($container) {
+ $logService = new LogService($container);
+ $logDocument = $logService->logRequest($container['request']);
+
+ return $logDocument;
+ };
+
+ // Merge in specific Web settings
+ $container['view'] = function ($c) {
+ $view = new \Slim\Views\Twig(dirname(__FILE__).'/View/V10/OAuth/Templates', [
+ 'debug' => 'true',
+ 'cache' => dirname(__FILE__).'/View/V10/OAuth/Templates',
+ ]);
+ $twigDebug = new \Twig_Extension_Debug();
+ $view->addExtension($twigDebug);
+
+ return $view;
+ };
+
+ // Auth - token
+ $container['accessToken'] = function ($container) {
+ // CORS
+ if ($container['request']->isOptions()) {
+ return null;
+ }
+ // Public routes
+ if ($container['request']->getUri()->getPath() === '/about') {
+ return null;
+ }
+ if (strpos($container['request']->getUri()->getPath(), '/oauth') === 0) {
+ return null;
+ }
+
+ $basicAuthService = new BasicAuthService($container);
+ $oAuthService = new OAuthService($container);
+
+ $token = null;
+
+ try {
+ $token = $oAuthService->extractToken($container['request']);
+ } catch (AuthFailureException $e) {
+ // Ignore
+ }
+
+ try {
+ $token = $basicAuthService->extractToken($container['request']);
+ //$container['requestLog']->addRelation('basicToken', $token)->save();
+ } catch (AuthFailureException $e) {
+ // Ignore
+ }
+
+ if (null === $token) {
+ throw new HttpException('Credentials invalid!', Controller::STATUS_UNAUTHORIZED);
+ }
+
+ return $token;
+ };
+
+ // Create Auth service (empty session at that stage)
+ $container['auth'] = function ($container) {
+ $authService = new AuthService($container);
+ $token = $container['accessToken'];
+ if (null !== $token) {
+ $token = $token->toArray();
+ $authService->register($token->userId, $token->permissions);
+ }
+ return $authService;
+ };
+
+ // Version
+ $container['version'] = function ($container) {
+ if ($container['request']->isOptions() || $container['request']->getUri()->getPath() === '/about' || $container['request']->getUri()->getPath() === '/oauth') {
+ $versionString = Config::get(['xAPI', 'latest_version']);
+ } else {
+ $versionString = $container['request']->getHeaderLine('X-Experience-API-Version');
+ }
+
+ if (!$versionString) {
+ throw new HttpException('X-Experience-API-Version header missing.', Controller::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $version = Versioning::fromString($versionString);
+ } catch (\InvalidArgumentException $e) {
+ throw new HttpException('X-Experience-API-Version header invalid.', Controller::STATUS_BAD_REQUEST);
+ }
+
+ if (!in_array($versionString, Config::get(['xAPI', 'supported_versions']))) {
+ throw new HttpException('X-Experience-API-Version is not supported.', Controller::STATUS_BAD_REQUEST);
+ }
+
+ return $version;
+ };
+
+ return $container;
+ }
+
+ /**
+ * Initialize php-cli configuration and load services
+ * @return \Psr\Container\ContainerInterface service container
+ */
+ public function initCliContainer($container = null)
+ {
+ $container = $this->initGenericContainer($container);
+
+ $logger = new Logger('cli');
+
+ $formatter = new \Monolog\Formatter\LineFormatter();
+
+ $handler = new \Monolog\Handler\StreamHandler('php://stdout');
+ $handler->setFormatter($formatter);
+ $logger->pushHandler($handler);
+
+ $container['logger'] = $logger;
+
+ return $container;
+ }
+
+ /**
+ * Boot web application (Slim App), including all routes
+ * @return \Psr\Http\Message\ResponseInterface
+ * @throws AppInitException
+ */
+ public function bootWebApp()
+ {
+ if (!self::$containerInstantiated) {
+ throw new AppInitException('Bootrstrap; You must initiate the Bootstrapper using the static factory!');
+ }
+
+ $container = self::$containerInstance;
+
+ $app = new SlimApp($container);
+
+ // Slim parser override and CORS compatibility layer (Internet Explorer)
+ $app->add(function ($request, $response, $next) use ($container) {
+
+ $request->registerMediaTypeParser('application/json', function ($input) {
+ return json_decode($input);
+ });
+
+ if ($request->isPost() && $request->getQueryParam('method')) {
+ $method = $request->getQueryParam('method');
+ $request = $request->withMethod($method);
+ mb_parse_str($request->getBody(), $postData);
+ $parameters = new Collection($postData);
+ if ($parameters->has('content')) {
+ $string = $parameters->get('content');
+ } else {
+ // Content is the only valid body parameter...everything else are either headers or query parameters
+ $string = '';
+ }
+
+ // Remove body, add headers
+ $parameters->remove('content');
+ $allowedHeaders = ['content-type', 'authorization', 'x-experience-api-version', 'content-length', 'if-match', 'if-none-match'];
+ foreach ($parameters as $key => $value) {
+ if (in_array(strtolower($key), $allowedHeaders)) {
+ $request = $request->withHeader($key, explode(',', $value));
+ $parameters->remove($key);
+ }
+ }
+
+ // Write the string into the body
+ $stream = fopen('php://memory', 'r+');
+ fwrite($stream, $string);
+ rewind($stream);
+ $body = new \Slim\Http\Stream($stream);
+ $request = $request->withBody($body)->reparseBody();
+
+ // Query string
+ $uri = $request->getUri();
+ $uri = $uri->withQuery(http_build_query($parameters->all()));
+ $request = $request->withUri($uri);
+
+ // Reparse the request - override request (sort of a hack)
+ $container->offsetUnset('request');
+ $container->offsetSet('request', $request);
+ //$container['parser']->parseRequest($request);
+ }
+
+ $response = $next($request, $response);
+
+ return $response;
+ });
+
+ ////
+ // ROUTER
+ ////
+
+ $router = new Routes();
+
+ ////
+ // Extensions
+ ////
+
+ // Load extensions (event listeners and routes) that may exist
+ $extensions = Config::get('extensions');
+
+ if ($extensions) {
+ foreach ($extensions as $extension) {
+ if ($extension['enabled'] === true) {
+ // Instantiate the extension class
+ $className = $extension['class_name'];
+ $extension = new $className($container);
+
+ // Load any xAPI event handlers added by the extension
+ $listeners = $extension->getEventListeners();
+ foreach ($listeners as $listener) {
+ $container['eventDispatcher']->addListener($listener['event'], [$extension, $listener['callable']], (isset($listener['priority']) ? $listener['priority'] : 0));
+ }
+
+ // Load any routes added by extension
+ $extensionRoutes = $extension->getRoutes();
+ $router->merge($extensionRoutes);
+ }
+ }
+ }
+
+ ////
+ // SlimApp
+ ////
+
+ // fetch routes after extensions have merged theirs
+ $routes = $router->all();
+
+ foreach ($routes as $pattern => $route) {
+ // register single route with methods and controller
+ $app->map($route['methods'], $pattern, function ($request, $response, $args) use ($container, $route) {
+ $resource = Controller::load($container, $request, $response, $route['controller']);
+ // We could also throw an Exception on load and catch it here...but that might have a performance penalty? It is definitely a cleaner, more proper option.
+ if ($resource instanceof \Psr\Http\Message\ResponseInterface) {
+ return $resource;
+ } else {
+ $method = strtolower($request->getMethod());
+ // HEAD method needs to respond exactly the same as GET method (minus the body)
+ // Body will be removed automatically by Slim
+ if ($method === 'head') {
+ $method = 'get';
+ }
+ return $resource->$method();
+ }
+ });
+ }
+
+ return $app;
+ }
+
+ /**
+ * Boot php-cli application (Symfony Console), including all commands
+ * @return \Symfony\Component\Console\Application instance
+ */
+ public function bootCliApp()
+ {
+ $app = new CliApp(self::$containerInstance);
+ return $app;
+ }
+
+ /**
+ * Empty placeholder for unit testing
+ * @return void
+ */
+ public function bootTest()
+ {
+ // Expose container instance so Tests can inject it into services (or use it)
+ return self::$containerInstance;
+ }
+}
diff --git a/src/xAPI/Collection/Attachments.php b/src/xAPI/Collection/Attachments.php
deleted file mode 100644
index 7ec429d9..00000000
--- a/src/xAPI/Collection/Attachments.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class Attachments extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Attachment';
- }
-}
diff --git a/src/xAPI/Collection/AuthScopes.php b/src/xAPI/Collection/AuthScopes.php
deleted file mode 100644
index f60d830b..00000000
--- a/src/xAPI/Collection/AuthScopes.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class AuthScopes extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Auth\\Scope';
- }
-}
diff --git a/src/xAPI/Collection/BasicTokens.php b/src/xAPI/Collection/BasicTokens.php
deleted file mode 100644
index c6c0f729..00000000
--- a/src/xAPI/Collection/BasicTokens.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class BasicTokens extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Auth\\BasicToken';
- }
-}
diff --git a/src/xAPI/Collection/Logs.php b/src/xAPI/Collection/Logs.php
deleted file mode 100644
index da84c7d4..00000000
--- a/src/xAPI/Collection/Logs.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class Logs extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Log';
- }
-}
diff --git a/src/xAPI/Collection/OAuthClients.php b/src/xAPI/Collection/OAuthClients.php
deleted file mode 100644
index ea0aa798..00000000
--- a/src/xAPI/Collection/OAuthClients.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class OAuthClients extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Auth\\OAuthClient';
- }
-}
diff --git a/src/xAPI/Collection/OAuthTokens.php b/src/xAPI/Collection/OAuthTokens.php
deleted file mode 100644
index 4d12bf47..00000000
--- a/src/xAPI/Collection/OAuthTokens.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class OAuthTokens extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Auth\\OAuthToken';
- }
-}
diff --git a/src/xAPI/Collection/PersistentSessions.php b/src/xAPI/Collection/PersistentSessions.php
deleted file mode 100644
index 45217e57..00000000
--- a/src/xAPI/Collection/PersistentSessions.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class PersistentSessions extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Auth\\PersistentSession';
- }
-}
diff --git a/src/xAPI/Collection/Statements.php b/src/xAPI/Collection/Statements.php
deleted file mode 100644
index 72639b7f..00000000
--- a/src/xAPI/Collection/Statements.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class Statements extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\Statement';
- }
-}
diff --git a/src/xAPI/Collection/Users.php b/src/xAPI/Collection/Users.php
deleted file mode 100644
index b777e853..00000000
--- a/src/xAPI/Collection/Users.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Collection;
-
-use Sokil\Mongo\Collection;
-
-class Users extends Collection
-{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\User';
- }
-}
diff --git a/src/xAPI/Command.php b/src/xAPI/Command.php
index 7f479ceb..e7c6b996 100644
--- a/src/xAPI/Command.php
+++ b/src/xAPI/Command.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,48 +25,34 @@
namespace API;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
-use Slim\Slim;
-
-// TODO - Derive Resource.php and Command.php from the same parent base class!!!
class Command extends SymfonyCommand
{
/**
- * @var \Slim\Slim
- */
- private $slim;
-
- /**
- * Construct.
+ * {@inheritDoc}
*/
public function __construct()
{
parent::__construct();
- $this->setSlim(Slim::getInstance());
$this->init();
}
/**
* Default init, use for overwrite only.
+ * {@inheritDoc}
*/
public function init()
{
}
/**
- * @return \Slim\Slim
- */
- public function getSlim()
- {
- return $this->slim;
- }
-
- /**
- * @param \Slim\Slim $slim
+ * Get service container
+ *
+ * @return Interop\Container\ContainerInterface
*/
- public function setSlim($slim)
+ public function getContainer()
{
- $this->slim = $slim;
+ return $this->getApplication()->getContainer();
}
}
diff --git a/src/xAPI/Config.php b/src/xAPI/Config.php
new file mode 100644
index 00000000..3706ca2b
--- /dev/null
+++ b/src/xAPI/Config.php
@@ -0,0 +1,154 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from slim.
+ * License information is available at https://github.com/slimphp/Slim/blob/3.x/LICENSE.md
+ *
+ */
+
+namespace API;
+
+use API\Util\Collection;
+use InvalidArgumentException;
+
+class Config
+{
+ private static $collection = null;
+
+ /**
+ * Initiate the Config (only callable once)
+ *
+ * - A Config collection can only be factored once
+ *
+ * @param array $data The data to initiate it with
+ *
+ * @return void
+ * @throws AppInitException if already initiated
+ * @throws InvalidArgumentException if $data is not an array (object instances would be mutable)
+ */
+ public static function factory($data = [])
+ {
+ if (self::$collection) {
+ throw new AppInitException('Config: Cannot be reinitiated');
+ }
+ if (!is_array($data)) {//PHP 5 & 7
+ throw new InvalidArgumentException('Config: $data must be an array');
+ }
+ self::$collection = new Collection($data);
+ }
+
+ /**
+ * Merge collection of items
+ *
+ * - New config items can be added freely, @see sConfig::set()
+ *
+ * @param array $data collection (assoziative array) of items
+ *
+ * @return void
+ * @throws AppInitException if one of the keys alredy exists
+ * @throws InvalidArgumentException if $data is not an array (object instances would be mutable)
+ */
+ public static function merge($data = [])
+ {
+ if (!self::$collection) {
+ throw new AppInitException('Config: You must call the factory before being able to get and set values!');
+ }
+ if (!is_array($data)) {//PHP 5 & 7
+ throw new InvalidArgumentException('Config: $data must be an array');
+ }
+ foreach ($data as $key => $value) {
+ self::set($key, $value);
+ }
+ }
+
+ /**
+ * Get collection item
+ *
+ * @param string $key|array The key(s) to get
+ * @param mixed $default optional return value
+ *
+ * @return mixed The value at this key
+ * @throws AppInitException
+ */
+ public static function get($key, $default = null)
+ {
+ if (!self::$collection) {
+ throw new AppInitException('Config: You must call the factory before being able to get and set values!');
+ }
+ return self::$collection->get($key, $default);
+ }
+
+ /**
+ * Get all items in collection
+ *
+ * @return array The collection's source data
+ * @throws AppInitException
+ */
+ public static function all()
+ {
+ if (!self::$collection) {
+ throw new AppInitException('Config: You must call the factory before being able to get and set values!');
+ }
+ return self::$collection->all();
+ }
+
+ /**
+ * Set collection item
+ *
+ * - New config items can be added freely, @see sConfig::set()
+ *
+ * @param array $data collection (assoziative array) of items
+ *
+ * @return void
+ * @throws AppInitException
+ * @throws InvalidArgumentException if key alredy exists
+ */
+ public static function set($key, $value)
+ {
+ if (!self::$collection) {
+ throw new AppInitException('Config: You must call the factory before being able to get and set values!');
+ }
+ if (self::$collection->has($key)) {
+ throw new InvalidArgumentException('Config: Cannot override existing Config property!');
+ }
+ self::$collection->set($key, $value);
+ }
+
+ /**
+ * Reset configuration (unit tests)
+ * @ignore do not compile to docs
+ * @return void
+ * @throws AppInitException if called from outside Bootstrap
+ */
+ public static function reset()
+ {
+ $trace = debug_backtrace();
+ $class = $trace[1]['class'];
+
+ if ($class === 'API\\Bootstrap') {
+ self::$collection = null;
+ return;
+ }
+
+ throw new AppInitException('Config: reset not allowed outside Bootstrap class');
+ }
+}
diff --git a/src/xAPI/Config/Config.development.yml b/src/xAPI/Config/Config.development.yml
deleted file mode 100644
index 5fcc903e..00000000
--- a/src/xAPI/Config/Config.development.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-# Development configuration overrides
-log.enable: true
-debug: true
diff --git a/src/xAPI/Config/Config.production.yml b/src/xAPI/Config/Config.production.yml
deleted file mode 100644
index 268c4cf9..00000000
--- a/src/xAPI/Config/Config.production.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-# Production configuration overrides
-log.enable: true
-debug: false
diff --git a/src/xAPI/Config/Config.template.yml b/src/xAPI/Config/Config.template.yml
deleted file mode 100644
index de260dfa..00000000
--- a/src/xAPI/Config/Config.template.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: BrightCookie LRS
-mode: production
-database:
- host_uri: 'mongodb://127.0.0.1'
- db_name: lxHive
-xAPI:
- latest_version: 1.0.2
- statement_get_limit: 100
- default_statement_get_format: exact
- supported_versions: [1.0.2]
- supported_auth_scopes: [{ name: super, description: 'Administrate the LRS' }, { name: statements/write, description: 'Write statements' }, { name: statements/read, description: 'Read statements' }, { name: statements/read/mine, description: 'Read own statements' }, { name: attachments, description: 'Include attachments' }, { name: state, description: 'Access the State API' }, { name: profile, description: 'Access the Profile API' }, { name: define, description: '(Re)define new Activities' }, { name: all/read, description: 'Full read-only access' }, { name: all, description: 'Full access' }]
- oauth: { reauthorize_time: 0, reauthenticate_time: 2592000, branding: { enabled: false, logo_path: null, header: null, footer: null, css_path: null } }
-filesystem:
- exposed_url: /attachments
- in_use: local
- local: { root_dir: '%app.root%/storage/files' }
diff --git a/src/xAPI/Config/Templates/Config.development.yml b/src/xAPI/Config/Templates/Config.development.yml
new file mode 100644
index 00000000..0b8a64d8
--- /dev/null
+++ b/src/xAPI/Config/Templates/Config.development.yml
@@ -0,0 +1,6 @@
+# Development configuration overrides
+log:
+ enabled: true
+ handlers: [ErrorLogHandler, FirePHPHandler]
+ name: Development
+debug: true
diff --git a/src/xAPI/Config/Templates/Config.production.yml b/src/xAPI/Config/Templates/Config.production.yml
new file mode 100644
index 00000000..35d6f4a1
--- /dev/null
+++ b/src/xAPI/Config/Templates/Config.production.yml
@@ -0,0 +1,6 @@
+# Production configuration overrides
+log:
+ enabled: true
+ handlers: [StreamHandler]
+ name: Production
+debug: false
diff --git a/src/xAPI/Config/Templates/Config.yml b/src/xAPI/Config/Templates/Config.yml
new file mode 100644
index 00000000..7d3b3a6b
--- /dev/null
+++ b/src/xAPI/Config/Templates/Config.yml
@@ -0,0 +1,123 @@
+# LRS name (displayed in /about)
+name: lxHive
+# Runtime mode
+mode: production
+# App version, do not edit!
+version: 0.10.0
+####
+# Database
+# Do NOT make changes in production mode
+####
+storage:
+ in_use: Mongo
+ Mongo:
+ host_uri: 'mongodb://127.0.0.1'
+ db_name: lxHive
+####
+# Extensions
+# Register and configure lxHive extensions
+####
+extensions:
+ # Single extension entry with identifier
+ ExtendedQuery:
+ # Extension base class mapper
+ class_name: '\API\Extensions\ExtendedQuery\ExtendedQuery'
+ # Enable or disable extension
+ enabled: true
+####
+# xAPI spec related configuration
+####
+xAPI:
+ # main xAPI version to target
+ latest_version: 1.0.3
+ # limit of documents to return per call
+ statement_get_limit: 100
+ # statement return format
+ default_statement_get_format: exact
+ # legacy xAPI versions (include main version as well)
+ supported_versions: [1.0.0, 1.0.1, 1.0.2, 1.0.3]
+ ####
+ # Authentication and permission matrix, we recommended not to change this in production LRS
+ #
+ # - changes do only apply to NEW users and tokens
+ # - permission inheritance: one-level inheritance only
+ # - a parent inherits the assigned permissions but not the children of the inherited permissions
+ # - inherited permission names must also be defined as top-level permissions
+ #
+ # https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#authentication
+ ####
+ supported_auth_scopes:
+ # client can create statements
+ 'statements/write':
+ module: xAPI
+ description: 'Write statements'
+ inherits: ['attachments']
+ # client can read own statement documents only
+ 'statements/read/mine':
+ module: xAPI
+ description: 'Read own statements'
+ inherits: ['attachments', 'ext/extendedquery/statements']
+ # client can read any statement
+ 'statements/read':
+ module: xAPI
+ description: 'Read statements'
+ inherits: ['attachments', 'ext/extendedquery/statements']
+ # client can crud any state dcument
+ 'state':
+ module: xAPI
+ description: 'Access the State API'
+ inherits: []
+ # client can crud any state dcument
+ 'profile':
+ module: xAPI
+ description: 'Access the Profile API'
+ inherits: []
+ # client can define global activity documents
+ 'define':
+ module: xAPI
+ description: '(Re)define new Activities'
+ inherits: []
+ # client can read any document
+ 'all/read':
+ module: xAPI
+ description: 'Full read-only access'
+ inherits: ['statements/read/mine', 'attachments', 'state', 'profile', 'attachments', 'ext/extendedquery/statements']
+ # client can read/write any document
+ 'all':
+ module: xAPI
+ description: 'Full access'
+ inherits: ['statements/write', 'statements/read/mine', 'statements/read', 'state', 'define', 'profile', 'all/read', 'attachments', 'ext/extendedquery/statements']
+ # client is lrs adminstrator
+ 'super':
+ module: System
+ description: 'Administrate the LRS'
+ inherits: ['statements/write', 'statements/read/mine', 'statements/read', 'state', 'define', 'profile', 'all/read', 'all', 'attachments', 'ext/extendedquery/statements']
+ # client can store statement attachment documents
+ 'attachments':
+ module: System
+ description: 'Include attachments'
+ inherits: []
+ # Extension permissions - TODO: Refactor extension permissions in 0.11.0
+ 'ext/extendedquery/statements':
+ module: Extension
+ description: 'Extended statement querying'
+ inherits: []
+ ####
+ # OAuth2 configuration
+ ####
+ oauth:
+ # oauth pages branding
+ branding:
+ enabled: false
+ logo_path: null
+ header: null
+ footer: null
+ css_path: null
+####
+# File storage configuration
+####
+filesystem:
+ exposed_url: /attachments
+ in_use: local
+ local:
+ root_dir: '../storage/files'
diff --git a/src/xAPI/Config/Templates/UnitTest.yml b/src/xAPI/Config/Templates/UnitTest.yml
new file mode 100644
index 00000000..1a437db6
--- /dev/null
+++ b/src/xAPI/Config/Templates/UnitTest.yml
@@ -0,0 +1 @@
+description: 'A test yml file for unit testing'
diff --git a/src/xAPI/Console/Application.php b/src/xAPI/Console/Application.php
new file mode 100644
index 00000000..35461834
--- /dev/null
+++ b/src/xAPI/Console/Application.php
@@ -0,0 +1,44 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Console;
+
+use Symfony\Component\Console\Application as SymfonyApplication;
+use API\BaseTrait;
+
+class Application extends SymfonyApplication
+{
+ use BaseTrait;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function __construct($container = null, $name = 'UNKNOWN', $version = 'UNKNOWN')
+ {
+ if ($container) {
+ $this->setContainer($container);
+ }
+ parent::__construct($name, $version);
+ }
+}
diff --git a/src/xAPI/Console/AuthScopeCreateCommand.php b/src/xAPI/Console/AuthScopeCreateCommand.php
deleted file mode 100644
index 0d5f7bd0..00000000
--- a/src/xAPI/Console/AuthScopeCreateCommand.php
+++ /dev/null
@@ -1,62 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Console;
-
-use API\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\Question;
-use API\Service\Auth\OAuth as OAuthService;
-
-class AuthScopeCreateCommand extends Command
-{
- protected function configure()
- {
- $this
- ->setName('auth:scope:create')
- ->setDescription('Creates a new authentication scope!')
- ;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output)
- {
- $oAuthService = new OAuthService($this->getSlim());
-
- $helper = $this->getHelper('question');
-
- $question = new Question('Please enter a name (scope identifier): ', 'untitled');
- $name = $helper->ask($input, $output, $question);
-
- $question = new Question('Please enter a description: ', '');
- $description = $helper->ask($input, $output, $question);
-
- $scope = $oAuthService->addScope($name, $description);
- $text = json_encode($scope, JSON_PRETTY_PRINT);
-
- $output->writeln('Auth scope successfully created! ');
- $output->writeln('Info: ');
- $output->writeln($text);
- }
-}
diff --git a/src/xAPI/Console/BasicTokenCreateCommand.php b/src/xAPI/Console/BasicTokenCreateCommand.php
index e8f2a5b2..fe725296 100644
--- a/src/xAPI/Console/BasicTokenCreateCommand.php
+++ b/src/xAPI/Console/BasicTokenCreateCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,47 +30,45 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\ChoiceQuestion;
-use API\Service\Auth\Basic as BasicAuthService;
-use API\Service\User as UserService;
-use API\Service\AuthScopes as AuthScopesService;
+use API\Admin\Auth;
+use API\Admin\User;
class BasicTokenCreateCommand extends Command
{
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
->setName('auth:basic:create')
->setDescription('Creates a new basic auth token')
->setDefinition(
- new InputDefinition(array(
- new InputOption('email', 'e', InputOption::VALUE_OPTIONAL),
+ new InputDefinition([
new InputOption('name', 'na', InputOption::VALUE_OPTIONAL),
new InputOption('description', 'd', InputOption::VALUE_OPTIONAL),
new InputOption('expiration', 'x', InputOption::VALUE_OPTIONAL),
+ new InputOption('email', 'e', InputOption::VALUE_OPTIONAL),
new InputOption('scopes', 's', InputOption::VALUE_OPTIONAL),
new InputOption('key', 'k', InputOption::VALUE_OPTIONAL),
new InputOption('secret', 'sc', InputOption::VALUE_OPTIONAL),
- ))
+ new InputOption('userid', 'u', InputOption::VALUE_OPTIONAL),
+ ])
)
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $basicAuthService = new BasicAuthService($this->getSlim());
- $scopesService = new AuthScopesService($this->getSlim());
-
- $userService = new UserService($this->getSlim());
- $userService->fetchAll();
+ $authAdmin = new Auth($this->getContainer());
+ $userAdmin = new User($this->getContainer());
- $users = [];
- foreach ($userService->getCursor() as $user) {
- $users[$user->get('email')] = $user;
- }
-
- $helper = $this->getHelper('question');
+ // TODO 0.11.x paginated query
+ $users = $userAdmin->fetchAllUserEmails();
$output->writeln([
'==========================>',
@@ -82,53 +80,39 @@ protected function execute(InputInterface $input, OutputInterface $output)
'',
]);
- // get permissions from user service. Abort if no permissions set
- if (!$scopesService->count()) {
- throw new \RuntimeException(
- 'No oAuth scopes found. Please run command setup:oauth first'
- );
+ if (!count($users)) {
+ throw new \RuntimeException('No registered users found');
}
- // intro
- $question = new ConfirmationQuestion('Continue? (y/n) ', false);
- if (!$helper->ask($input, $output, $question)) {
- $output->writeln('Process aborted by user. ');
- return 0;
- }
-
- // email
+ // 1. email
if (null === $input->getOption('email')) {
+ $helper = $this->getHelper('question');
$question = new Question('Please enter the email of the associated user: ', '');
$question->setAutocompleterValues(array_keys($users));
$question->setNormalizer(function ($value) {
return $value ? trim(strtolower($value)) : '';
});
-
$question->setValidator(function ($answer) use ($users, $output) {
if (!is_string($answer) || empty($answer)) {
throw new \RuntimeException(
'Invalid input!'
);
}
-
if ('exit' === $answer) {
return $answer;
}
-
if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) {
throw new \RuntimeException(
'Invalid email address!'
);
}
-
if (!isset($users[$answer])) {
$output->writeln(' - Hint: Type exit to exit this dialog and return to the console.');
throw new \RuntimeException(
'No user record for "'.$answer.'" found! '
);
}
-
return $answer;
});
$question->setMaxAttempts(null);
@@ -138,81 +122,72 @@ protected function execute(InputInterface $input, OutputInterface $output)
$output->writeln('Process aborted by user. ');
return 0;
}
- $user = $users[$email];
+ } else {
+ $email = $input->getOption('email');
}
+ $user = $users[$email];
- // name
+ // 2. Name
if (null === $input->getOption('name')) {
+ $helper = $this->getHelper('question');
$question = new Question('Please enter a name: ', 'untitled');
$name = $helper->ask($input, $output, $question);
} else {
$name = $input->getOption('name');
}
- // description
+ // 3. description
if (null === $input->getOption('description')) {
+ $helper = $this->getHelper('question');
$question = new Question('Please enter a description: ', '');
$description = $helper->ask($input, $output, $question);
} else {
$description = $input->getOption('description');
}
- // expiration
+ // 4. expiration period
if (null === $input->getOption('expiration')) {
+ $helper = $this->getHelper('question');
$question = new Question('Please enter the expiration timestamp for the token (blank == indefinite): ');
$expiresAt = $helper->ask($input, $output, $question);
} else {
$expiresAt = $input->getOption('expiration');
}
- // permissions
- $map = array_map(function ($val) {
- return $val->getName();
- }, $user->permissions);
- $_scopes = array_values($map);
- $scopes = $_scopes;
-
- // Brightcookie/lxHive-Internal#125 fetch and merge child permissions for displaying options
- foreach ($_scopes as $scope) {
- $scopes += $scopesService->getChildrenFor($scope);
- }
- $scopesDictionary = $scopesService->findByNames($scopes, true);
-
+ // 5. scopes
if (null === $input->getOption('scopes')) {
+ $helper = $this->getHelper('question');
$question = new ChoiceQuestion(
- 'Please select which scopes you would like to enable (defaults to super). Separate multiple values with commas (without spaces). If you select super, all other permissions are also inherited: ',
- array_keys($scopesDictionary),
+ implode("\n", [
+ 'Token Permissions: ',
+ ' * Please select which user permissions you would like to enable.',
+ ' * Separate multiple values with commas (without spaces).',
+ ]),
+ $user->permissions, // USER permissions!
'0'
);
$question->setMultiselect(true);
- $selectedScopeNames = $helper->ask($input, $output, $question);
-
- $selectedScopes = [];
- foreach ($selectedScopeNames as $selectedScopeName) {
- $selectedScopes[] = $scopesDictionary[$selectedScopeName];
- }
+ $permissions = $helper->ask($input, $output, $question);
} else {
- $selectedScopeNames = explode(',', $input->getOption('scopes'));
+ $permissions = explode(',', $input->getOption('scopes'));
}
- $selectedScopes = [];
- foreach ($selectedScopeNames as $selectedScopeName) {
- $selectedScopes[] = $scopesDictionary[$selectedScopeName];
- }
-
- $token = $basicAuthService->addToken($name, $description, $expiresAt, $user, $selectedScopes);
+ $output->writeln(' * Selected permissions: '. implode(', ', $permissions));
+ $permissions = $userAdmin->mergeInheritedPermissions($permissions);
+ $output->writeln(' * with inherited permissions: '. implode(', ', $permissions));
+ // 6. store token
+ $key = null;
if (null !== $input->getOption('key')) {
- $token->setKey($input->getOption('key'));
- $token->save();
+ $key = $input->getOption('key');
}
-
+ $secret = null;
if (null !== $input->getOption('secret')) {
- $token->setSecret($input->getOption('secret'));
- $token->save();
+ $secret = $input->getOption('secret');
}
+ $token = $authAdmin->addToken($name, $description, $expiresAt, $user, $permissions, $key, $secret);
- $text = json_encode($token, JSON_PRETTY_PRINT);
+ $text = json_encode($token, JSON_PRETTY_PRINT);
$output->writeln('Basic token successfully created! ');
$output->writeln('Info: ');
diff --git a/src/xAPI/Console/BasicTokenDeleteCommand.php b/src/xAPI/Console/BasicTokenDeleteCommand.php
index 79376f11..ea4dcfa0 100644
--- a/src/xAPI/Console/BasicTokenDeleteCommand.php
+++ b/src/xAPI/Console/BasicTokenDeleteCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,10 +29,13 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;
-use API\Service\Auth\Basic as AccessTokenService;
+use API\Admin\Auth;
class BasicTokenDeleteCommand extends Command
{
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
@@ -41,30 +44,29 @@ protected function configure()
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $helper = $this->getHelper('question');
- $accessTokenService = new AccessTokenService($this->getSlim());
-
- $accessTokenService->fetchTokens();
- $keys = [];
- foreach ($accessTokenService->getCursor() as $document) {
- $keys[] = $document->getKey();
- }
+ $authAdmin = new Auth($this->getContainer());
+ $keys = $authAdmin->listBasicTokenIds();
+ // 1. key
+ $helper = $this->getHelper('question');
$question = new Question('Please enter the key of the token you wish to delete: ');
$question->setAutocompleterValues($keys);
-
$key = $helper->ask($input, $output, $question);
+ //2. confirm
$question = new ConfirmationQuestion('Are you sure (y/n): ', false);
-
if (!$helper->ask($input, $output, $question)) {
return;
}
- $accessTokenService->deleteToken($key);
+ // 2. delete
+ $authAdmin->deleteBasicToken($key);
- $output->writeln('Basic token successfully deleted! ');
+ $output->writeln('Token successfully deleted! ');
}
}
diff --git a/src/xAPI/Console/BasicTokenExpireCommand.php b/src/xAPI/Console/BasicTokenExpireCommand.php
index 56d9c421..6d008b27 100644
--- a/src/xAPI/Console/BasicTokenExpireCommand.php
+++ b/src/xAPI/Console/BasicTokenExpireCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,10 +29,13 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ConfirmationQuestion;
-use API\Service\Auth\Basic as AccessTokenService;
+use API\Admin\Auth;
class BasicTokenExpireCommand extends Command
{
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
@@ -41,29 +44,28 @@ protected function configure()
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $helper = $this->getHelper('question');
- $accessTokenService = new AccessTokenService($this->getSlim());
-
- $accessTokenService->fetchTokens();
- $keys = [];
- foreach ($accessTokenService->getCursor() as $document) {
- $keys[] = $document->getKey();
- }
+ $authAdmin = new Auth($this->getContainer());
+ $keys = $authAdmin->listBasicTokenIds();
+ // 1. key
+ $helper = $this->getHelper('question');
$question = new Question('Please enter the key of the token you wish to expire: ');
$question->setAutocompleterValues($keys);
-
$key = $helper->ask($input, $output, $question);
+ // 2. confirm
$question = new ConfirmationQuestion('Are you sure (y/n): ', false);
-
if (!$helper->ask($input, $output, $question)) {
return;
}
- $accessTokenService->expireToken($key);
+ //3. expire document
+ $authAdmin->expireBasicToken($key);
$output->writeln('Token successfully expired! ');
}
diff --git a/src/xAPI/Console/BasicTokenListCommand.php b/src/xAPI/Console/BasicTokenListCommand.php
index a433c5fd..9eb0d949 100644
--- a/src/xAPI/Console/BasicTokenListCommand.php
+++ b/src/xAPI/Console/BasicTokenListCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,10 +27,15 @@
use API\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-use API\Service\Auth\Basic as AccessTokenService;
+use API\Admin\Auth;
+
+// TODO 0.11.x review, command seems to have no real use
class BasicTokenListCommand extends Command
{
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
@@ -39,16 +44,13 @@ protected function configure()
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $accessTokenService = new AccessTokenService($this->getSlim());
-
- $accessTokenService->fetchTokens();
-
- $textArray = [];
- foreach ($accessTokenService->getCursor() as $document) {
- $textArray[] = $document->jsonSerialize();
- }
+ $authAdmin = new Auth($this->getContainer());
+ $textArray = $authAdmin->listBasicTokens();
$text = json_encode($textArray, JSON_PRETTY_PRINT);
diff --git a/src/xAPI/Console/LrsReportCommand.php b/src/xAPI/Console/LrsReportCommand.php
new file mode 100644
index 00000000..56ffb12a
--- /dev/null
+++ b/src/xAPI/Console/LrsReportCommand.php
@@ -0,0 +1,145 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Console;
+
+use API\Command;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Helper\TableSeparator;
+
+use API\Admin\LrsReport;
+
+class LrsReportCommand extends Command
+{
+ /**
+ * @inheritDoc
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('status')
+ ->setDescription('Runs a basic health report on your LRS')
+ ;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $report = new LrsReport();
+ $result = $report->check();
+ $summary = $report->summary();
+
+ // 1. reports
+ $table = new Table($output);
+ $table->setHeaders([
+ 'item', 'status', 'message', 'notes'
+ ]);
+ foreach ($result as $title => $section) {
+ $this->renderTableSection($title, $section, $output, $table);
+ }
+
+ $table->render();
+
+ // 2. summary
+ $s = [];
+
+ $s[] = ($summary['total'] > 0 && $summary['completed'] == $summary['total']) ? $this->style('success', 'Complete') : $this->style('error', 'Aborted');
+ $s[] = 'finished '. $this->style('bold', $summary['completed'].'/'.$summary['total']. ' sections');
+
+ $s[] = 'with '. $this->style('bold', $summary['reports']['total'].' checks');
+ foreach ($summary['reports'] as $status => $count) {
+ if ($status == 'total') {
+ continue;
+ }
+ $s[] = $status.': '.(($count > 0) ? $this->style($status, $count) : $count);
+ }
+ $output->writeln(implode(', ', $s));
+ }
+
+ /**
+ * Render a report section into a Symfony console table
+ * @see LrsReport::check()
+ *
+ * @param string $caption section title
+ * @param string $result LrsReport::check() result item
+ * @param OutputInterface $output
+ * @param Table $table
+ *
+ * @return void
+ */
+ protected function renderTableSection($caption, $result, OutputInterface $output, Table $table)
+ {
+ $table->addRow(new TableSeparator());
+ $table->addRow([
+ $this->style('caption', $caption)
+ ]);
+ $table->addRow(new TableSeparator());
+
+ foreach ($result as $item => $data) {
+ $table->addRow([
+ $item,
+ $this->style($data['status'], $data['status']),
+ $data['value'],
+ $data['note'],
+ ]);
+ }
+ }
+
+ /**
+ * Styles a console message
+ * @see http://symfony.com/doc/current/console/coloring.html
+ *
+ * @param string $status see \API\Admin\LrsReport
+ * @param string $message
+ * @return string
+ */
+ protected function style($status, $message)
+ {
+ switch ($status) {
+ case 'bold': {
+ return ''.$message.'>';
+ }
+ case 'caption': {
+ return ''.$message.'>';
+ }
+ case 'success': {
+ return ''.$message.'>';
+ }
+ case 'error': {
+ return ''.$message.'>';
+ }
+ case 'warn': {
+ return ''.$message.'>';
+ }
+ }
+
+ return $message;
+ }
+}
diff --git a/src/xAPI/Console/OAuthClientCreateCommand.php b/src/xAPI/Console/OAuthClientCreateCommand.php
index 1f431235..d8f96afd 100644
--- a/src/xAPI/Console/OAuthClientCreateCommand.php
+++ b/src/xAPI/Console/OAuthClientCreateCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,10 +28,15 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
-use API\Service\Auth\OAuth as OAuthService;
+
+use API\Admin\Auth;
+use API\Admin;
class OAuthClientCreateCommand extends Command
{
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
@@ -40,22 +45,41 @@ protected function configure()
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $oAuthService = new OAuthService($this->getSlim());
+ $authAdmin = new Auth($this->getContainer());
+ $validator = new Admin\Validator();
+ // 1. name
$helper = $this->getHelper('question');
-
- $question = new Question('Please enter a name: ', 'untitled');
+ $question = new Question('Please enter a name: ', '');
+ $question->setMaxAttempts(null);
+ $question->setValidator(function ($answer) use ($validator) {
+ $validator->validateName($answer);
+ return $answer;
+ });
$name = $helper->ask($input, $output, $question);
+ // 2. description
+ $helper = $this->getHelper('question');
$question = new Question('Please enter a description: ', '');
$description = $helper->ask($input, $output, $question);
+ // 3. redirect Uri
+ $helper = $this->getHelper('question');
$question = new Question('Please enter a redirect URI: ');
+ $question->setMaxAttempts(null);
+ $question->setValidator(function ($answer) use ($validator) {
+ $validator->validateRedirectUri($answer);
+ return $answer;
+ });
$redirectUri = $helper->ask($input, $output, $question);
- $client = $oAuthService->addClient($name, $description, $redirectUri);
+ // 4. write record
+ $client = $authAdmin->addOAuthClient($name, $description, $redirectUri);
$text = json_encode($client, JSON_PRETTY_PRINT);
$output->writeln('OAuth client successfully created! ');
diff --git a/src/xAPI/Console/OAuthClientListCommand.php b/src/xAPI/Console/OAuthClientListCommand.php
index 60dd0542..9db2ea4a 100644
--- a/src/xAPI/Console/OAuthClientListCommand.php
+++ b/src/xAPI/Console/OAuthClientListCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,10 +27,16 @@
use API\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-use API\Service\Auth\OAuth as OAuthService;
+use API\Admin\Auth;
+
+// TODO 0.11.x review, command seems to have no real use
class OAuthClientListCommand extends Command
{
+
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
@@ -39,17 +45,13 @@ protected function configure()
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $oAuthService = new OAuthService($this->getSlim());
-
- $oAuthService->fetchClients();
-
- $textArray = [];
- foreach ($oAuthService->getCursor() as $document) {
- $textArray[] = $document->jsonSerialize();
- }
-
+ $authAdmin = new Auth($this->getContainer());
+ $textArray = $authAdmin->listOAuthClients();
$text = json_encode($textArray, JSON_PRETTY_PRINT);
$output->writeln('Clients successfully fetched! ');
diff --git a/src/xAPI/Console/SetupCommand.php b/src/xAPI/Console/SetupCommand.php
new file mode 100644
index 00000000..26de0149
--- /dev/null
+++ b/src/xAPI/Console/SetupCommand.php
@@ -0,0 +1,284 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Console;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Console\Command\Command as SymfonyCommand;
+
+use API\Config;
+use API\Bootstrap;
+use API\Admin\Setup;
+use API\Admin\Validator;
+
+use RunTimeException;
+use API\AppInitException;
+use API\Admin\AdminException;
+
+class SetupCommand extends SymfonyCommand
+{
+ /**
+ * @var Setup $setup
+ */
+ private $setup;
+
+ /**
+ * @var Validator $validator
+ */
+ private $validator;
+
+ /**
+ * @var array $sequence
+ */
+ private $sequence = [
+ 'ioCheckConfig' => 'Install default configuration',
+ 'ioSetLrsInstance' => 'Configure Lrs instance',
+ 'ioSetMongoStorage' => 'Configure Mongo database',
+
+ 'reboot' => 'Reboot app with new configuration',
+
+ 'ioVerifyDatabaseVersion' => 'Verify Database compatibility',
+ 'ioInstallDatabaseSchema' => 'Install lxHive database schemas',
+ 'ioInstallAuthScopes' => 'Setup oAuth scopes',
+ 'ioSetLocalFileStorage' => 'Setup local file storage'
+ ];
+
+ /**
+ * @constructor
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ $this->setup = new Setup();
+ $this->validator = new Validator();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('setup')
+ ->setDescription('Sets up lxHive')
+ ;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $io->title('Welcome to the setup of lxHive! ');
+ $io->newLine();
+
+ // check config
+ Bootstrap::factory(Bootstrap::None);
+ if ($this->setup->locateYaml('Config.yml')) {
+ throw new RuntimeException('A `Config.yml` file exists already. The LRS configuration would be overwritten. To restore the defaults you must manually remove the file first.');
+ }
+
+ $count = 0;
+ foreach ($this->sequence as $callback => $title) {
+ $count++;
+ $io->section('['.$count.'/'.count($this->sequence).'] '.$title);
+ call_user_func_array([$this, $callback], [$io]);
+ }
+
+ // finish
+ $io->success('Setup complete!');
+ $io->text(' --> NEXT: Create your first user with ./X user:create ');
+ $io->newLine();
+ }
+
+ /**
+ * Installs default configruation files from templates
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws AppInitException
+ */
+ private function reboot($io)
+ {
+ Bootstrap::reset();
+ Bootstrap::factory(Bootstrap::Console);
+ $io->listing(['Re-booted app']);
+ }
+
+ /**
+ * Installs default configruation files from templates
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws AdminException
+ */
+ private function ioCheckConfig($io)
+ {
+ $msg = [];
+
+ $this->setup->installYaml('Config.yml');
+ $msg[] = 'Config.yml installed';
+
+ $this->setup->removeYaml('Config.production.yml');
+ $this->setup->installYaml('Config.production.yml');
+ $msg[] = 'Config.production.yml installed';
+
+ $this->setup->removeYaml('Config.development.yml');
+ $this->setup->installYaml('Config.development.yml');
+ $msg[] = 'Config.development.yml installed';
+
+ $io->listing($msg);
+ }
+
+ /**
+ * Installs default configruation files from templates
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws AdminException
+ */
+ private function ioSetLrsInstance($io)
+ {
+ $name = $io->ask('Enter a name for this lxHive instance: ', 'lxHive', function ($answer) {
+ $this->validator->validateName($answer);
+ return $answer;
+ });
+
+ $this->setup->updateYaml('Config.yml', [
+ 'name' => $name
+ ]);
+ $io->listing(['lxHive instance: '. $name]);
+ }
+
+ /**
+ * Set Mongo Databas connection
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws AdminException
+ */
+ private function ioSetMongoStorage($io)
+ {
+ $msg = [];
+
+ $host = $io->ask('Enter the URI of your MongoDB installation:', 'mongodb://127.0.0.1', function ($answer) use ($io) {
+ $conn = $this->setup->testDbConnection($answer);
+ if (!$conn) {
+ throw new RuntimeException('Connection unsuccessful, please try again.');
+ }
+ return $answer;
+ });
+ $msg[] = 'Mongo connection: '. $host;
+
+ $db = $io->ask('Enter the name of your MongoDB database:', 'lxHive', function ($answer) {
+ $this->validator->validateMongoName($answer);
+ return $answer;
+ });
+ $msg[] = 'Mongo database: '. $db;
+
+ $this->setup->updateYaml('Config.yml', [
+ 'storage' => [
+ 'in_use' => 'Mongo',
+ 'Mongo' => [
+ 'host_uri' => $host,
+ 'db_name'=> $db,
+ ]
+ ]
+ ]);
+ $io->listing($msg);
+ }
+
+ /**
+ * Check Database compatibility
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws RuntimeException
+ */
+ private function ioVerifyDatabaseVersion($io)
+ {
+ $msg = $this->setup->verifyDbVersion(); // throws exception on fail
+ $io->listing(['DB is compatible: ' . $msg]);
+ }
+
+ /**
+ * Installs Database schemas
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws RuntimeException
+ */
+ private function ioInstallDatabaseSchema($io)
+ {
+ $this->setup->installDb(); // throws exception on fail
+ $io->listing(['DB schema installed']);
+ }
+
+ /**
+ * Installs AuthScopes
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws RuntimeException
+ */
+ private function ioInstallAuthScopes($io)
+ {
+ // logic migrated to Auth service, left for notification
+ $io->listing(['AuthScopes installed']);
+ }
+
+ /**
+ * Installs local files torage
+ * @param Symfony\Component\Console\Style\SymfonyStyle $io
+ *
+ * @return void
+ * @throws RuntimeException
+ */
+ private function ioSetLocalFileStorage($io)
+ {
+ $msg = [];
+
+ try {
+ $info = $this->setup->installFileStorage();
+ $owner = posix_getpwuid($info->getOwner());
+ $group = posix_getgrgid($info->getGroup());
+
+ $msg[] = '' .$info->getPath();
+ $msg[] = 'permission: ' .substr(sprintf('%o', $info->getPerms()), -4);
+ $msg[] = 'path: '.$info->getRealPath();
+ $msg[] = 'owner: '.$owner['name'];
+ $msg[] = 'group: '.$group['name'];
+ } catch (AdminException $e) {
+ $io->warning('Unable to create Local file Storage. Please create manually.');
+ $io->text('Error message: '.$e->getMessage());
+ }
+
+ $io->listing($msg);
+ $io->note('Please make sure your webserver has read/write access to the "storage" directories.');
+ }
+}
diff --git a/src/xAPI/Console/SetupDbCommand.php b/src/xAPI/Console/SetupDbCommand.php
deleted file mode 100644
index a07c2cfd..00000000
--- a/src/xAPI/Console/SetupDbCommand.php
+++ /dev/null
@@ -1,104 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Console;
-
-use API\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Input\ArrayInput;
-use Symfony\Component\Console\Question\Question;
-use Symfony\Component\Yaml\Yaml;
-use Sokil\Mongo\Client;
-
-class SetupDbCommand extends Command
-{
- protected function configure()
- {
- $this
- ->setName('setup:db')
- ->setDescription('Sets up the MongoDB database')
- ;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output)
- {
- $output->writeln('Welcome to the setup of lxHive! ');
-
- // instance name
- $helper = $this->getHelper('question');
- $question = new Question('Enter a name for this lxHive instance: ', 'Untitled');
- $name = $helper->ask($input, $output, $question);
- $output->writeln('x Instance name set to "' .$name.'"!');
-
- // Mongo connection
- $connectionSuccess = false;
- while (!$connectionSuccess) {
- $question = new Question('Enter the URI of your MongoDB installation (default: "mongodb://127.0.0.1"): ', 'mongodb://127.0.0.1');
- $mongoHostname = $helper->ask($input, $output, $question);
-
- $client = new Client($mongoHostname);
- try {
- $mongoVersion = $client->getDbVersion();
- $output->writeln('x Connection successful, MongoDB version '.$mongoVersion.'.');
- $connectionSuccess = true;
- } catch (\MongoConnectionException $e) {
- $output->writeln('! Connection unsuccessful, please try again.');
- }
- }
-
- // Mongo database
- $question = new Question('Enter the name of your MongoDB database (default: "lxHive"): ', 'lxHive');
- $mongoDatabase = $helper->ask($input, $output, $question);
- $output->writeln('x DB setup complete!');
-
- // Merge and store config.yml
- $currentConfig = Yaml::parse(file_get_contents(__DIR__.'/../Config/Config.template.yml'));
- $mergingArray = ['name' => $name, 'database' => ['host_uri' => $mongoHostname, 'db_name' => $mongoDatabase]];
- $newConfig = array_merge($currentConfig, $mergingArray);
- $yamlData = Yaml::dump($newConfig);
- file_put_contents(__DIR__.'/../Config/Config.yml', $yamlData);
- $output->writeln('x Configuration saved!');
-
- // oAuth scopes
- // @TODO retireve collection name from service
- $output->writeln('Setting up default OAuth scopes...');
- $mongo = new \API\Util\MongoClient($newConfig);
- $collection = $mongo->db->getCollection('authScopes');
- foreach ($newConfig['xAPI']['supported_auth_scopes'] as $scope) {
- $exists = $collection->find()->where('name', $scope['name'])->findOne();
- if ($exists) {
- $output->writeln(' - skip scope '.$exists->get('name').' exits already.');
- } else{
- $output->writeln(' - new scope '.$scope['name'].' added.');
- }
- $document = $collection->createDocument($scope);
- $document->save();
- }
- $output->writeln('x OAuth scopes configured!');
-
- $output->writeln('Setup complete! ');
-
- }
-}
diff --git a/src/xAPI/Console/SetupOAuthCommand.php b/src/xAPI/Console/SetupOAuthCommand.php
deleted file mode 100644
index 7946ec00..00000000
--- a/src/xAPI/Console/SetupOAuthCommand.php
+++ /dev/null
@@ -1,58 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Console;
-
-use API\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use API\Service\Auth\OAuth as OAuthService;
-
-class SetupOAuthCommand extends Command
-{
- protected function configure()
- {
- $this
- ->setName('setup:oauth')
- ->setDescription('Sets up default OAuth scopes')
- ;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output)
- {
- $output->writeln('Setting up default OAuth scopes... ');
-
- $oAuthService = new OAuthService($this->getSlim());
- foreach ($this->getSlim()->config('xAPI')['supported_auth_scopes'] as $authScope) {
- $scope = $oAuthService->addScope($authScope['name'], $authScope['description']);
- if (!$scope) {
- $output->writeln(' - skip scope '.$authScope['name'].' exits already.');
- } else{
- $output->writeln(' - new scope '.$authScope['name'].' added.');
- }
- }
-
- $output->writeln('OAuth scopes configured! ');
- }
-}
diff --git a/src/xAPI/Console/UserCreateCommand.php b/src/xAPI/Console/UserCreateCommand.php
index b9824604..ec280169 100644
--- a/src/xAPI/Console/UserCreateCommand.php
+++ b/src/xAPI/Console/UserCreateCommand.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,138 +24,166 @@
namespace API\Console;
-use API\Command;
+use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\Question;
+use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Question\ChoiceQuestion;
-use API\Service\User as UserService;
-use API\Service\AuthScopes as AuthScopesService;
+
+use Symfony\Component\Console\Command\Command as SymfonyCommand;
+
+use API\Config;
+use API\Bootstrap;
+use API\Command;
+use API\Admin\Setup;
+use API\Admin\Validator;
+
+use API\Admin;
+use API\Admin\User as UserAdmin;
+
+use RunTimeException;
class UserCreateCommand extends Command
{
+ /**
+ * @var UserAdmin $userAdmin;
+ */
+ private $userAdmin;
+
+ /**
+ * @var Validator $validator
+ */
+ private $validator;
+
+ /**
+ * {@inheritDoc}
+ */
protected function configure()
{
$this
->setName('user:create')
->setDescription('Creates a new user')
->setDefinition(
- new InputDefinition(array(
+ new InputDefinition([
+ new InputOption('name', 'na', InputOption::VALUE_OPTIONAL),
+ new InputOption('description', 'd', InputOption::VALUE_OPTIONAL),
new InputOption('email', 'e', InputOption::VALUE_OPTIONAL),
new InputOption('password', 'p', InputOption::VALUE_OPTIONAL),
new InputOption('permissions', 'pm', InputOption::VALUE_OPTIONAL),
- ))
+ ])
)
;
}
+ /**
+ * {@inheritDoc}
+ */
protected function execute(InputInterface $input, OutputInterface $output)
{
- $userService = new UserService($this->getSlim());
- $scopesService = new AuthScopesService($this->getSlim());
- $helper = $this->getHelper('question');
-
- // abort if no permissions set
- if (!$scopesService->count()) {
- throw new \RuntimeException(
- 'No oAuth scopes found. Please run command setup:oauth first'
- );
- }
+ $io = new SymfonyStyle($input, $output);
- // email
- $question = new Question('Please enter an e-mail: ', '');
- $question->setMaxAttempts(null);
- $question->setValidator(function ($answer) {
- $this->validateEmail($answer);
- return $answer;
- });
- $email = $helper->ask($input, $output, $question);
-
- // password
- $question = new Question('Please enter a password: ', '');
- $question->setMaxAttempts(null);
- $question->setValidator(function ($answer) {
- $this->validatePassword($answer);
- return $answer;
- });
- $password = $helper->ask($input, $output, $question);
-
- // permissions
- $permissionsDictionary = $scopesService->fetchAll(true);
- if (null === $input->getOption('permissions')) {
- $question = new ChoiceQuestion(
- 'Please select which permissions you would like to enable (defaults to super). Separate multiple values with commas (without spaces). If you select super, all other permissions are also inherited: ',
- array_keys($permissionsDictionary),
- '0'
- );
- $question->setMultiselect(true);
+ $io->title('Create a new user. ');
+ $io->newLine();
- $selectedPermissionNames = $helper->ask($input, $output, $question);
+ $this->userAdmin = new UserAdmin($this->getContainer());
+ $this->validator = new Validator();
+
+ // 1. Name
+ if (null === $input->getOption('name')) {
+ $name = $io->ask('[1/6] Please enter a name', null, function ($answer) {
+ $this->validator->validateName($answer);
+ return $answer;
+ });
} else {
- $selectedPermissionNames = explode(',', $input->getOption('permissions'));
+ $name = $input->getOption('name');
}
- $selectedPermissions = [];
- foreach ($selectedPermissionNames as $selectedPermissionName) {
- $selectedPermissions[] = $permissionsDictionary[$selectedPermissionName];
+ // 2. Description
+ if (null === $input->getOption('description')) {
+ $description = $io->ask('[2/6] Please enter a description', false);
+ } else {
+ $description = $input->getOption('description');
}
- $user = $userService->addUser($email, $password, $selectedPermissions);
- $text = json_encode($user, JSON_PRETTY_PRINT);
-
- $userCount = $userService->getEmailCount($email);
- if ($userCount > 1) {
- $output->writeln('Note: there are ' . $userCount . ' duplicate accounts with the same email - ' . $email . ' ');
+ // 3. Email
+ if (null === $input->getOption('email')) {
+ $email = $io->ask('[3/6] Please enter an e-mail', null, function ($answer) {
+ $this->validator->validateEmail($answer);
+ return $answer;
+ });
+ } else {
+ $email = $input->getOption('email');
}
- $output->writeln('User successfully created! ');
- $output->writeln('Info: ');
- $output->writeln($text);
- }
+ // 4. Password
+ if (null === $input->getOption('password')) {
+ $password = $io->askHidden('[4/6] Please enter a password', function ($answer) {
+ $this->validator->validatePassword($answer);
+ return $answer;
+ });
+ } else {
+ $password = $input->getOption('password');
+ }
+ if (null === $input->getOption('password')) {
+ $confirmed = $io->askHidden('[5/6] Please confirm password', function ($answer) use ($password) {
+ if ($answer !== $password) {
+ throw new RuntimeException('Passwords do not match');
+ }
+ return $answer;
+ });
+ } else {
+ $confirmed = true;
+ }
- /**
- * Validate password
- * @param string $str
- *
- * @return void
- * @throws AdminException
- */
- public function validatePassword($str)
- {
- $errors = [];
- $length = 6;
+ // 5. Permissions
+ $scopes = [];
+ $scopes = $this->userAdmin->fetchAvailablePermissions();
- if (strlen($str) < $length) {
- $errors[] = 'Must have at least '.$length.' characters';
+ if (null === $input->getOption('permissions')) {
+ $question = new ChoiceQuestion(
+ implode("\n", [
+ '[6/6] Please select which permissions you would like to enable.',
+ ' * Separate multiple values with commas (without spaces).',
+ ]),
+ array_keys($scopes),
+ null
+ );
+ $question->setMultiselect(true);
+ $question->setMaxAttempts(null);
+ $permissions = $io->askQuestion($question);// validation by ChoiceQuestion
+ } else {
+ $permissions = explode(',', $input->getOption('permissions'));
}
- if (!preg_match('/[0-9]+/', $str)) {
- $errors[] = 'Must include at least one number.';
- }
+ $io->text(' * Selected permissions: '. implode(', ', $permissions));
+ $permissions = $this->userAdmin->mergeInheritedPermissions($permissions);
+ $io->text(' * with inherited permissions: '. implode(', ', $permissions));
- if (!preg_match('/[a-zA-Z]+/', $str)) {
- $errors[] = 'Must include at least one letter.';
- }
+ // 6. add record
+ $cursor = $this->userAdmin->addUser($name, $description, $email, $password, $permissions);
- if (!empty($errors)) {
- throw new \RuntimeException(json_encode($errors));
- }
- }
+ $io->success('User successfully created!');
- /**
- * Validate email address
- * @param string $email
- *
- * @return void
- * @throws AdminException
- */
- public function validateEmail($email)
- {
- if (!filter_var($email, \FILTER_VALIDATE_EMAIL)) {
- throw new \RuntimeException('Invalid email address!');
- }
+ // 7. display
+ $io->text(' !!! Please store below user information privately and secure!');
+ $io->newLine();
+
+ $user = $cursor->toArray();
+
+ $io->listing([
+ 'id : '.$cursor->getId(),
+ 'name : '.$user->name,
+ 'email : '.$user->email,
+ 'password : '.$password,
+ 'permissions : '.implode(', ', $permissions),
+ ]);
+
+ $io->text(' --> NEXT: Create a basic token with ./X auth:basic:create ');
+ $io->text('or');
+ $io->text(' --> NEXT: Create an oAuth token with ./X oauth:client:create ');
+ $io->newLine();
}
}
diff --git a/src/xAPI/Container.php b/src/xAPI/Container.php
new file mode 100644
index 00000000..37c6a2ff
--- /dev/null
+++ b/src/xAPI/Container.php
@@ -0,0 +1,118 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+use Interop\Container\ContainerInterface;
+use Pimple\Container as PimpleContainer;
+
+/**
+ * Lightweight Interop\Container\ContainerInterface compliant implementation of Symfony's Pimple container
+ *
+ * @see \Pimple\Container
+ * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md
+ */
+class Container extends PimpleContainer implements ContainerInterface
+{
+ /**
+ * @var bool $locked prevents overwriting of any (not just services) properties if set to true
+ */
+ private $locked = false;
+
+ /**
+ * @inheritdoc
+ */
+ public function __construct(array $values = [])
+ {
+ parent::__construct($values);
+ }
+
+ /**
+ * Locks a container, all existing and new properties cannot be overwritten
+ * @return void
+ */
+ public function lock()
+ {
+ $this->locked = true;
+ }
+
+ /**
+ * Gets a parameter or an object.
+ * $this->get('doesnotexist') will throw an ContainerException exception
+ * $this->get('doesnotexist', null) will return null (or any other value) instead
+ * The first one is recommended if using it as a service store, the second for a pure value store
+ *
+ * @param string $id The unique identifier for the parameter or object
+ * @return mixed The value for $id ore return arg, if it was set
+ * @throws ContainerException Thrown if no entry was found for this identifier and no return arg was set.
+ * @throws \InvalidArgumentException
+ */
+ public function get($id)
+ {
+ if (!$this->offsetExists($id)) {
+ // return an optional value
+ if (func_num_args() > 1) {
+ return func_get_arg(1);
+ }
+ throw new ContainerException(sprintf('Property "%s" does not exist.', $id));
+ }
+ return $this->offsetGet($id);
+ }
+
+ /**
+ * Sets a parameter or an object.
+ *
+ * @param string $id The unique identifier for the parameter or object
+ * @param mixed $value The value of the parameter or a closure to define an object
+ *
+ * @throws \RuntimeException Prevent override of a frozen service
+ */
+ public function set($id, $value)
+ {
+ $this->offsetSet($id, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function offsetSet($id, $value)
+ {
+ if ($this->offsetExists($id) && $this->locked) {
+ throw new ContainerException(sprintf('Cannot override "%s"inside a locked collection.', $id));
+ }
+ parent::offsetSet($id, $value);
+ }
+
+ /**
+ * Returns true if the container can return an entry for the given identifier.
+ * Returns false otherwise.
+ *
+ * @param string $id Identifier of the entry to look for.
+ *
+ * @return boolean
+ */
+ public function has($id)
+ {
+ return $this->offsetExists($id);
+ }
+}
diff --git a/src/xAPI/Collection/Activities.php b/src/xAPI/ContainerException.php
similarity index 75%
rename from src/xAPI/Collection/Activities.php
rename to src/xAPI/ContainerException.php
index 2fadfbf0..cd0f7a40 100644
--- a/src/xAPI/Collection/Activities.php
+++ b/src/xAPI/ContainerException.php
@@ -1,9 +1,8 @@
.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+use API\Controller\Error as Error;
+use API\View\Error as ErrorView;
+use Psr\Http\Message\ResponseInterface;
+use API\Config;
+
+abstract class Controller
+{
+ use BaseTrait;
+
+ const STATUS_OK = 200;
+ const STATUS_CREATED = 201;
+ const STATUS_ACCEPTED = 202;
+ const STATUS_NO_CONTENT = 204;
+
+ const STATUS_MULTIPLE_CHOICES = 300;
+ const STATUS_MOVED_PERMANENTLY = 301;
+ const STATUS_FOUND = 302;
+ const STATUS_NOT_MODIFIED = 304;
+ const STATUS_USE_PROXY = 305;
+ const STATUS_TEMPORARY_REDIRECT = 307;
+
+ const STATUS_BAD_REQUEST = 400;
+ const STATUS_UNAUTHORIZED = 401;
+ const STATUS_FORBIDDEN = 403;
+ const STATUS_NOT_FOUND = 404;
+ const STATUS_NOT_FOUND_MESSAGE = 'Cannot find requested resource.';
+ const STATUS_METHOD_NOT_ALLOWED = 405;
+ const STATUS_METHOD_NOT_ALLOWED_MESSAGE = 'Method %s is not allowed on this resource.';
+ const STATUS_NOT_ACCEPTED = 406;
+ const STATUS_CONFLICT = 409;
+ const STATUS_PRECONDITION_FAILED = 412;
+ const STATUS_TOO_MANY_REQUESTS = 429;
+ const STATUS_BANDIWDTH_LIMIT_EXCEEDED = 509;
+
+ const STATUS_INTERNAL_SERVER_ERROR = 500;
+ const STATUS_NOT_IMPLEMENTED = 501;
+
+ /**
+ * Request.
+ */
+ public $request;
+
+ /**
+ * Response.
+ */
+ public $response;
+
+ /**
+ * Construct.
+ */
+ public function __construct($container, $request, $response)
+ {
+ $this->setContainer($container);
+ $this->setRequest($request);
+ $this->setResponse($response);
+
+ $this->init();
+ }
+
+ /**
+ * Default init, use for overwrite only.
+ */
+ public function init()
+ {
+ }
+
+ /**
+ * Default (empty) GET handler
+ * @return Psr\Http\Message\ResponseInterface A reponse
+ */
+ public function get()
+ {
+ return $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'GET'));
+ }
+
+ /**
+ * Default (empty) POST handler
+ * @return Psr\Http\Message\ResponseInterface A reponse
+ */
+ public function post()
+ {
+ return $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'POST'));
+ }
+
+ /**
+ * Default (empty) PUT handler
+ * @return Psr\Http\Message\ResponseInterface A reponse
+ */
+ public function put()
+ {
+ return $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'PUT'));
+ }
+
+ /**
+ * Default (empty) DELETE handler
+ * @return Psr\Http\Message\ResponseInterface A reponse
+ */
+ public function delete()
+ {
+ return $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'DELETE'));
+ }
+
+ /**
+ * Default (empty) OPTIONS handler
+ * @return Psr\Http\Message\ResponseInterface A reponse
+ */
+ public function options()
+ {
+ return $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'OPTIONS'));
+ }
+
+ /**
+ * @param int $status HTTP status code
+ * @param array $data The data
+ * @param array $allow Allowed methods
+ */
+ public function response($status = 200, $data = null, $allow = [])
+ {
+ if ($data instanceof ResponseInterface) {
+ $this->response = $data;
+ } else {
+ $body = $this->response->getBody();
+ $body->write($data);
+ }
+
+ $date = \API\Util\Date::dateTimeToISO8601(\API\Util\Date::dateTimeExact());
+
+ $this->response = $this->response->withStatus($status)
+ ->withHeader('Access-Control-Allow-Origin', '*')
+ ->withHeader('Access-Control-Allow-Methods', 'POST,PUT,GET,OPTIONS,DELETE')
+ ->withHeader('Access-Control-Allow-Headers', 'Origin,Content-Type,Authorization,Accept,X-Experience-API-Version,If-Match,If-None-Match')
+ ->withHeader('Access-Control-Allow-Credentials-Control-Allow-Origin', 'true')
+ ->withHeader('Access-Control-Expose-Headers', 'ETag,Last-Modified,Content-Length,X-Experience-API-Version,X-Experience-API-Consistent-Through')
+ ->withHeader('X-Experience-API-Version', Config::get(['xAPI', 'latest_version']))
+ ->withHeader('X-Experience-API-Consistent-Through', $date);
+
+ if (!empty($allow)) {
+ $this->response = $this->response->withHeader('Allow', strtoupper(implode(',', $allow)));
+ }
+
+ return $this->response;
+ }
+
+ public function jsonResponse($status = 200, $data = [], $allow = [])
+ {
+ if ($data instanceof ResponseInterface) {
+ $this->response = $data;
+ } else {
+ $this->response = $this->response->withJson($data, $status);
+ }
+
+ $date = \API\Util\Date::dateTimeToISO8601(\API\Util\Date::dateTimeExact());
+
+ $this->response = $this->response->withStatus($status)
+ ->withHeader('Access-Control-Allow-Origin', '*')
+ ->withHeader('Access-Control-Allow-Methods', 'POST,PUT,GET,OPTIONS,DELETE')
+ ->withHeader('Access-Control-Allow-Headers', 'Origin,Content-Type,Authorization,Accept,X-Experience-API-Version,If-Match,If-None-Match')
+ ->withHeader('Access-Control-Allow-Credentials-Control-Allow-Origin', 'true')
+ ->withHeader('Access-Control-Expose-Headers', 'ETag,Last-Modified,Content-Length,X-Experience-API-Version,X-Experience-API-Consistent-Through')
+ ->withHeader('X-Experience-API-Version', Config::get(['xAPI', 'latest_version']))
+ ->withHeader('X-Experience-API-Consistent-Through', $date);
+
+ if (!empty($allow)) {
+ $this->response = $this->response->withHeader('Allow', strtoupper(implode(',', $allow)));
+ }
+
+ return $this->response;
+ }
+
+ /**
+ * Error handler.
+ *
+ * @param int $code Error code
+ * @param string $message Error message
+ * @param array|object $data extra data to display
+ * @return string
+ */
+ public function error($code, $message = '', $data = [])
+ {
+ return $this->jsonResponse($code, ['code' => $code, 'message' => $message, 'data' => $data]);
+ }
+
+ /**
+ * Dynamically load a routing resource
+ * @param string $version xAPI version
+ * @param \Interop\Container\ContainerInterface service container
+ * @param \Psr\Http\Message\ServerRequestInterface $request Slim request instance
+ * @param \Psr\Http\Message\ResponseInterface $response Slim response instance
+ * @param string $resource the main resource
+ * @param string $subResource An optional subresource
+ *
+ * @return \API\ControllerInterface
+ */
+ public static function load($container, $request, $response, $controllerClass)
+ {
+ if (!class_exists($controllerClass)) {
+ $errorResource = new Error($container, $request, $response);
+ $errorResource = $errorResource->error(self::STATUS_NOT_FOUND, 'Cannot find requested resource.');
+
+ return $errorResource;
+ }
+
+ return new $controllerClass($container, $request, $response);
+ }
+
+ /**
+ * Gets the Request.
+ *
+ * @return mixed
+ */
+ public function getRequest()
+ {
+ return $this->request;
+ }
+
+ /**
+ * Sets the Request.
+ *
+ * @param mixed $request the request
+ *
+ * @return self
+ */
+ public function setRequest($request)
+ {
+ $this->request = $request;
+
+ return $this;
+ }
+
+ /**
+ * Gets the Response.
+ *
+ * @return mixed
+ */
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+ /**
+ * Sets the Response.
+ *
+ * @param mixed $response the response
+ *
+ * @return self
+ */
+ public function setResponse($response)
+ {
+ $this->response = $response;
+
+ return $this;
+ }
+}
diff --git a/src/xAPI/Controller/Error.php b/src/xAPI/Controller/Error.php
new file mode 100644
index 00000000..404027f4
--- /dev/null
+++ b/src/xAPI/Controller/Error.php
@@ -0,0 +1,9 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10;
+
+use API\Config;
+use API\Bootstrap;
+use API\Controller;
+use API\View\V10\About as AboutView;
+
+class About extends Controller
+{
+ /**
+ * Compile and render GET /about response
+ *
+ * @return void
+ */
+ public function get()
+ {
+ $versions = Config::get(['xAPI', 'supported_versions']);
+ $extensions = $this->getExtensionInfo();
+ $core = [
+ 'lrs' => [
+ 'name' => Config::get('name'),
+ 'mode' => Config::get('mode'),
+ 'version' => Bootstrap::VERSION,
+ ]
+ ];
+
+ $view = new AboutView($this->getResponse(), $this->getContainer(), [
+ 'versions' => $versions,
+ 'extensions' => array_merge($core, $extensions),
+ ]);
+ $view = $view->render();
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ /**
+ * Compile and render OPTIONS /about response
+ *
+ * @return void
+ */
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'GET'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Collect info about extensions
+ *
+ * @return array $info
+ */
+ public function getExtensionInfo()
+ {
+ $info = [];
+ $installed = Config::get(['extensions']);
+ foreach ($installed as $name => $ext) {
+ // Precaution in case of mis-configuration
+ try {
+ if ($ext['enabled']) {
+ $className = $ext['class_name'];
+ $instance = new $className($this->getContainer());
+ $info[$name] = $instance->about();
+ }
+ } catch (\Exception $e) {
+ // Do nothing
+ }
+ return $info;
+ }
+ }
+}
diff --git a/src/xAPI/Resource/V10/Activities.php b/src/xAPI/Controller/V10/Activities.php
similarity index 60%
rename from src/xAPI/Resource/V10/Activities.php
rename to src/xAPI/Controller/V10/Activities.php
index c656b4c1..9292f9dc 100644
--- a/src/xAPI/Resource/V10/Activities.php
+++ b/src/xAPI/Controller/V10/Activities.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,14 +22,13 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10;
+namespace API\Controller\V10;
-use API\Resource;
-use Slim\Helper\Set;
+use API\Controller;
use API\Service\Activity as ActivityService;
use API\View\V10\Activity as ActivityView;
-class Activities extends Resource
+class Activities extends Controller
{
/**
* @var \API\Service\Activity
@@ -41,31 +40,29 @@ class Activities extends Resource
*/
public function init()
{
- $this->setActivityService(new ActivityService($this->getSlim()));
+ $this->activityService = new ActivityService($this->getContainer());
}
// Boilerplate code until this is figured out...
public function get()
{
- $request = $this->getSlim()->request();
-
// Check authentication
- $this->getSlim()->auth->checkPermission('profile');
+ $this->getContainer()->get('auth')->requirePermission('profile');
- $this->activityService->activityGet($request);
+ $activityDocument = $this->activityService->activityGet();
// Render them
- $view = new ActivityView(['service' => $this->activityService]);
+ $view = new ActivityView($this->getResponse(), $this->getContainer());
- $view = $view->renderGetSingle();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
+ $view = $view->renderGetSingle($activityDocument);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
}
public function options()
{
//Handle options request
- $this->getSlim()->response->headers->set('Allow', 'GET');
- Resource::response(Resource::STATUS_OK);
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'GET'));
+ return $this->response(Controller::STATUS_OK);
}
/**
@@ -77,18 +74,4 @@ public function getActivityService()
{
return $this->activityService;
}
-
- /**
- * Sets the value of activityService.
- *
- * @param \API\Service\Activity $activityService the activity service
- *
- * @return self
- */
- public function setActivityService(\API\Service\Activity $activityService)
- {
- $this->activityService = $activityService;
-
- return $this;
- }
}
diff --git a/src/xAPI/Controller/V10/Activities/Profile.php b/src/xAPI/Controller/V10/Activities/Profile.php
new file mode 100644
index 00000000..2e676cbb
--- /dev/null
+++ b/src/xAPI/Controller/V10/Activities/Profile.php
@@ -0,0 +1,127 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10\Activities;
+
+use API\Controller;
+use API\Service\ActivityProfile as ActivityProfileService;
+use API\View\V10\ActivityProfile as ActivityProfileView;
+
+class Profile extends Controller
+{
+ /**
+ * @var \API\Service\ActivityProfile
+ */
+ private $activityProfileService;
+
+ /**
+ * Get activity service.
+ */
+ public function init()
+ {
+ $this->activityProfileService = new ActivityProfileService($this->getContainer());
+ }
+
+ /**
+ * Handle the Statement GET request.
+ */
+ public function get()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+ $documentResult = $this->activityProfileService->activityProfileGet();
+
+ // Render
+ $view = new ActivityProfileView($this->getResponse(), $this->getContainer());
+
+ if ($documentResult->getIsSingle()) {
+ $view = $view->renderGetSingle($documentResult);
+ return $this->response(Controller::STATUS_OK, $view);
+ } else {
+ $view = $view->renderGet($documentResult);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+ }
+
+ public function put()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Save the statements
+ $documentResult = $this->activityProfileService->activityProfilePut();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function post()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Save the statements
+ $documentResult = $this->activityProfileService->activityProfilePost();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function delete()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Save the statements
+ $deletionResult = $this->activityProfileService->activityProfileDelete();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,PUT,GET,DELETE'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Gets the value of activityProfileService.
+ *
+ * @return \API\Service\ActivityProfile
+ */
+ public function getActivityProfileService()
+ {
+ return $this->activityProfileService;
+ }
+}
diff --git a/src/xAPI/Controller/V10/Activities/State.php b/src/xAPI/Controller/V10/Activities/State.php
new file mode 100644
index 00000000..5e6806ec
--- /dev/null
+++ b/src/xAPI/Controller/V10/Activities/State.php
@@ -0,0 +1,130 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10\Activities;
+
+use API\Controller;
+use API\Service\ActivityState as ActivityStateService;
+use API\View\V10\ActivityState as ActivityStateView;
+
+class State extends Controller
+{
+ /**
+ * @var \API\Service\ActivityState
+ */
+ private $activityStateService;
+
+ /**
+ * Get activity service.
+ */
+ public function init()
+ {
+ $this->activityStateService = new ActivityStateService($this->getContainer());
+ }
+
+ /**
+ * Handle the Statement GET request.
+ */
+ public function get()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('state');
+
+ // TODO 0.11.x request validation
+
+ $documentResult = $this->activityStateService->activityStateGet();
+
+ // Render them
+ $view = new ActivityStateView($this->getResponse(), $this->getContainer());
+
+ if ($documentResult->getIsSingle()) {
+ $view = $view->renderGetSingle($documentResult);
+ return $this->response(Controller::STATUS_OK, $view);
+ } else {
+ $view = $view->renderGet($documentResult);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+ }
+
+ public function put()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('state');
+
+ // TODO 0.11.x request validation
+
+ // Save the state
+ $documentResult = $this->activityStateService->activityStatePut();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function post()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('state');
+
+ // Do the validation - TODO!!!
+ //$this->statementValidator->validateRequest($request);
+ //$this->statementValidator->validatePutRequest($request);
+
+ // Save the state
+ $documentResult = $this->activityStateService->activityStatePost();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function delete()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('state');
+
+ // TODO 0.11.x request validation
+
+ // Save the state
+ $documentResult = $this->activityStateService->activityStateDelete();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,PUT,GET,DELETE'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Gets the value of activityStateService.
+ *
+ * @return \API\Service\ActivityState
+ */
+ public function getActivityStateService()
+ {
+ return $this->activityStateService;
+ }
+}
diff --git a/src/xAPI/Resource/V10/Agents.php b/src/xAPI/Controller/V10/Agents.php
similarity index 53%
rename from src/xAPI/Resource/V10/Agents.php
rename to src/xAPI/Controller/V10/Agents.php
index ab409e78..aac09e01 100644
--- a/src/xAPI/Resource/V10/Agents.php
+++ b/src/xAPI/Controller/V10/Agents.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,39 +22,45 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10;
+namespace API\Controller\V10;
-use API\Resource;
-use Slim\Helper\Set;
+use API\Controller;
use API\View\V10\Agent as AgentView;
+use API\Util\Collection;
-class Agents extends Resource
+class Agents extends Controller
{
+ /**
+ * Handler for GET call
+ * @return Psr\Http\Message\ResponseInterface
+ */
public function get()
{
- $request = $this->getSlim()->request();
-
// Check authentication
- $this->getSlim()->auth->checkPermission('profile');
+ $this->getContainer()->get('auth')->requirePermission('profile');
- // TODO: Validation.
+ // TODO 0.11.x request validation
- $params = new Set($request->get());
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
$agent = $params->get('agent');
-
$agent = json_decode($agent, true);
- $view = new AgentView(['agent' => $agent]);
- $view = $view->renderGet();
+ $view = new AgentView($this->getResponse(), $this->getContainer());
+ $view = $view->renderGet($agent);
- Resource::jsonResponse(Resource::STATUS_OK, $view);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
}
+ /**
+ * Handler for OPTIONS call
+ * @return Psr\Http\Message\ResponseInterface
+ */
public function options()
{
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'GET');
- Resource::response(Resource::STATUS_OK);
+ // Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'GET'));
+ return $this->response(Controller::STATUS_OK);
}
}
diff --git a/src/xAPI/Controller/V10/Agents/Profile.php b/src/xAPI/Controller/V10/Agents/Profile.php
new file mode 100644
index 00000000..f3355679
--- /dev/null
+++ b/src/xAPI/Controller/V10/Agents/Profile.php
@@ -0,0 +1,128 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10\Agents;
+
+use API\Controller;
+use API\Service\AgentProfile as AgentProfileService;
+use API\View\V10\AgentProfile as AgentProfileView;
+
+class Profile extends Controller
+{
+ /**
+ * @var \API\Service\AgentProfile
+ */
+ private $agentProfileService;
+
+ /**
+ * Get agent profile service.
+ */
+ public function init()
+ {
+ $this->agentProfileService = new AgentProfileService($this->getContainer());
+ }
+
+ /**
+ * Handle the Statement GET request.
+ */
+ public function get()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ $documentResult = $this->agentProfileService->agentProfileGet();
+
+ // Render them
+ $view = new AgentProfileView($this->getResponse(), $this->getContainer());
+
+ if ($documentResult->getIsSingle()) {
+ $view = $view->renderGetSingle($documentResult);
+ return $this->response(Controller::STATUS_OK, $view);
+ } else {
+ $view = $view->renderGet($documentResult);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+ }
+
+ public function put()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Save the profiles
+ $documentResult = $this->agentProfileService->agentProfilePut();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function post()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Save the profiles
+ $documentResult = $this->agentProfileService->agentProfilePost();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function delete()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('profile');
+
+ // TODO 0.11.x request validation
+
+ // Delete the profiles
+ $deletionResult = $this->agentProfileService->agentProfileDelete();
+
+ //Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,PUT,GET,DELETE'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Gets the value of agentProfileService.
+ *
+ * @return \API\Service\AgentProfile
+ */
+ public function getAgentProfileService()
+ {
+ return $this->agentProfileService;
+ }
+}
diff --git a/src/xAPI/Resource/V10/Attachments.php b/src/xAPI/Controller/V10/Attachments.php
similarity index 53%
rename from src/xAPI/Resource/V10/Attachments.php
rename to src/xAPI/Controller/V10/Attachments.php
index 83939c5e..c9184df6 100644
--- a/src/xAPI/Resource/V10/Attachments.php
+++ b/src/xAPI/Controller/V10/Attachments.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,13 +22,13 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10;
+namespace API\Controller\V10;
-use API\Resource;
+use API\Controller;
use API\Service\Attachment as AttachmentService;
-use Slim\Helper\Set;
+use API\Util;
-class Attachments extends Resource
+class Attachments extends Controller
{
/**
* @var \API\Service\Attachment
@@ -40,63 +40,41 @@ class Attachments extends Resource
*/
public function init()
{
- $this->setAttachmentService(new AttachmentService($this->getSlim()));
+ $this->attachmentService = new AttachmentService($this->getContainer());
}
public function get()
{
- $request = $this->getSlim()->request();
+ $request = $this->getContainer()->get('parser')->getData();
// Check authentication
- $this->getSlim()->auth->checkPermission('attachments');
+ $this->getContainer()->get('auth')->requirePermission('attachments');
- $params = new Set($request->get());
+ $params = new Util\Collection($request->getParameters());
if (!$params->has('sha2')) {
- throw new \Exception('Missing sha2 parameter!', Resource::STATUS_BAD_REQUEST);
+ throw new \Exception('Missing sha2 parameter!', Controller::STATUS_BAD_REQUEST);
}
$sha2 = $params->get('sha2');
-
$encoding = $params->get('encoding');
+
// Fetch attachment metadata and data
$metadata = $this->attachmentService->fetchMetadataBySha2($sha2);
- $data = $this->attachmentService->fetchFileBySha2($sha2);
+ $data = $this->attachmentService->fetchFileBySha2($sha2);
if ($encoding !== 'binary') {
$data = base64_encode($data);
}
- $this->getSlim()->response->headers->set('Content-Type', $metadata->getContentType());
- Resource::response(Resource::STATUS_OK, $data);
+ $metadataDocument = new \API\Document\Generic($metadata);
+ $this->setResponse($this->getResponse()->withHeader('Content-Type', $metadataDocument->getContentType()));
+
+ return $this->response(Controller::STATUS_OK, $data);
}
public function options()
{
//Handle options request
- $this->getSlim()->response->headers->set('Allow', 'GET');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of attachmentService.
- *
- * @return \API\Service\Attachment
- */
- public function getAttachmentService()
- {
- return $this->attachmentService;
- }
-
- /**
- * Sets the value of attachmentService.
- *
- * @param \API\Service\Attachment $attachmentService the attachment service
- *
- * @return self
- */
- public function setAttachmentService(\API\Service\Attachment $attachmentService)
- {
- $this->attachmentService = $attachmentService;
-
- return $this;
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'GET'));
+ return $this->response(Controller::STATUS_OK);
}
}
diff --git a/src/xAPI/Controller/V10/Auth/Tokens.php b/src/xAPI/Controller/V10/Auth/Tokens.php
new file mode 100644
index 00000000..01270651
--- /dev/null
+++ b/src/xAPI/Controller/V10/Auth/Tokens.php
@@ -0,0 +1,125 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10\Auth;
+
+use API\Controller;
+use API\Service\Auth\Basic as BasicTokenService;
+use API\View\V10\BasicAuth\AccessToken as AccessTokenView;
+
+class Tokens extends Controller
+{
+ /**
+ * @var \API\Service\AccessToken
+ */
+ private $accessTokenService;
+
+ /**
+ * Get agent profile service.
+ */
+ public function init()
+ {
+ $this->accessTokenService = new BasicTokenService($this->getContainer());
+ }
+
+ public function get()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('super');
+
+ // TODO 0.11.x request validation
+
+ $this->accessTokenService->accessTokenGet();
+
+ // Render them
+ $view = new AccessTokenView($this->getResponse(), $this->getContainer());
+
+ $view = $view->render();
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ public function post()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('super');
+
+ // TODO 0.11.x request validation
+
+ $accessTokenDocument = $this->accessTokenService->accessTokenPost();
+
+ // Render them
+ $view = new AccessTokenView($this->getResponse(), $this->getContainer());
+
+ $view = $view->render($accessTokenDocument);
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ public function put()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('super');
+
+ // TODO 0.11.x request validation
+
+ $this->accessTokenService->accessTokenPut();
+
+ // Render them
+ $view = new AccessTokenView($this->getResponse(), $this->getContainer());
+
+ $view = $view->render();
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ public function delete()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('super');
+
+ // TODO 0.11.x request validation
+
+ $this->accessTokenService->accessTokenDelete();
+
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,PUT,GET,DELETE'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Gets the value of accessTokenService.
+ *
+ * @return \API\Service\AccessToken
+ */
+ public function getAccessTokenService()
+ {
+ return $this->accessTokenService;
+ }
+}
diff --git a/src/xAPI/Controller/V10/Oauth/Authorize.php b/src/xAPI/Controller/V10/Oauth/Authorize.php
new file mode 100644
index 00000000..86d39ab1
--- /dev/null
+++ b/src/xAPI/Controller/V10/Oauth/Authorize.php
@@ -0,0 +1,119 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10\Oauth;
+
+use API\Controller;
+use API\Service\Auth\OAuth as OAuthService;
+use API\Service\User as UserService;
+use API\View\V10\OAuth\Authorize as OAuthAuthorizeView;
+use API\Util\OAuth;
+
+class Authorize extends Controller
+{
+ /**
+ * @var \API\Service\Auth\OAuth
+ */
+ private $oAuthService;
+
+ /**
+ * @var \API\Service\User
+ */
+ private $userService;
+
+ /**
+ * Get agent profile service.
+ */
+ public function init()
+ {
+ $this->oAuthService = new OAuthService($this->getContainer());
+ $this->userService = new UserService($this->getContainer());
+ OAuth::loadSession();
+ }
+
+ public function get()
+ {
+ // TODO 0.11.x request validation
+
+ if ($this->userService->loggedIn()) {
+ $authorizeClientData = $this->oAuthService->authorizeGet();
+ // Authorization is always requested
+ $view = new OAuthAuthorizeView($this->getResponse(), $this->getContainer(), ['service' => $this->oAuthService]);
+ $user = $this->userService->getLoggedIn();
+ $client = $authorizeClientData;
+ $scopes = $authorizeClientData->scopes;
+ $view = $view->renderGet($user, $client, $scopes);
+ return $this->response(Controller::STATUS_OK, $view);
+ } else {
+ // Redirect to login
+ $redirectUrl = $this->getContainer()->get('url');
+ $redirectUrl->getPath()->remove('authorize');
+ $redirectUrl->getPath()->append('login');
+ $this->setResponse($this->getResponse()->withHeader('Location', $redirectUrl));
+ return $this->response(Controller::STATUS_FOUND);
+ }
+ }
+
+ public function post()
+ {
+ // TODO 0.11.x request validation
+
+ if ($this->userService->loggedIn()) {
+ // Authorization is always requested
+ $redirectUri = $this->oAuthService->authorizePost();
+ $this->setResponse($this->getResponse()->withHeader('Location', $redirectUri));
+ return $this->response(Controller::STATUS_FOUND);
+ } else {
+ // Unauthorized
+ return $this->response(Controller::STATUS_UNAUTHORIZED);
+ }
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,GET'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Gets the value of oAuthService.
+ *
+ * @return \API\Service\Auth\OAuth
+ */
+ public function getOAuthService()
+ {
+ return $this->oAuthService;
+ }
+
+ /**
+ * Gets the value of userService.
+ *
+ * @return \API\Service\User
+ */
+ public function getUserService()
+ {
+ return $this->userService;
+ }
+}
diff --git a/src/xAPI/Resource/V10/Oauth/Login.php b/src/xAPI/Controller/V10/Oauth/Login.php
similarity index 50%
rename from src/xAPI/Resource/V10/Oauth/Login.php
rename to src/xAPI/Controller/V10/Oauth/Login.php
index 017bc811..d2b2de17 100644
--- a/src/xAPI/Resource/V10/Oauth/Login.php
+++ b/src/xAPI/Controller/V10/Oauth/Login.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,15 +22,15 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10\Oauth;
+namespace API\Controller\V10\Oauth;
-use API\Resource;
+use API\Controller;
use API\Service\Auth\OAuth as OAuthService;
use API\Service\User as UserService;
use API\View\V10\OAuth\Login as LoginView;
use API\Util\OAuth;
-class Login extends Resource
+class Login extends Controller
{
/**
* @var \API\Service\Auth\OAuth
@@ -47,66 +47,60 @@ class Login extends Resource
*/
public function init()
{
- $this->setOAuthService(new OAuthService($this->getSlim()));
- $this->setUserService(new UserService($this->getSlim()));
+ $this->oAuthService = new OAuthService($this->getContainer());
+ $this->userService = new UserService($this->getContainer());
OAuth::loadSession();
}
public function get()
{
- $request = $this->getSlim()->request();
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
+ // TODO 0.11.x request validation
if (!$this->userService->loggedIn()) {
- $this->userService->loginGet($request);
+ $this->userService->loginGet();
// Authorization is always requested
- $view = new LoginView(['service' => $this->userService]);
+ $view = new LoginView($this->getResponse(), $this->getContainer(), ['service' => $this->oAuthService]);
$view = $view->renderGet();
- Resource::response(Resource::STATUS_OK, $view);
+ return $this->response(Controller::STATUS_OK, $view);
} else {
// Redirect to authorization
- $redirectUrl = $this->getSlim()->url;
+ $redirectUrl = $this->getContainer()->get('url');
$redirectUrl->getPath()->remove('login');
$redirectUrl->getPath()->append('authorize');
- $this->getSlim()->response->headers->set('Location', $redirectUrl);
- Resource::response(Resource::STATUS_FOUND);
+ $this->setResponse($this->getResponse()->withHeader('Location', $redirectUrl));
+ return $this->response(Controller::STATUS_FOUND);
}
}
public function post()
{
- $request = $this->getSlim()->request();
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
+ // TODO 0.11.x request validation
// Authorization is always requested
try {
- $this->userService->loginPost($request);
- $redirectUrl = $this->getSlim()->url;
+ // This sets the session for the user, otherwise throws an exception!
+ $this->userService->loginPost();
+ $redirectUrl = $this->getContainer()->get('url');
$redirectUrl->getPath()->remove('login');
$redirectUrl->getPath()->append('authorize');
- $this->getSlim()->response->headers->set('Location', $redirectUrl);
- Resource::response(Resource::STATUS_FOUND);
+ $this->setResponse($this->getResponse()->withHeader('Location', $redirectUrl));
+ return $this->response(Controller::STATUS_FOUND);
} catch (\Exception $e) {
- $view = new LoginView(['service' => $this->userService]);
- $view = $view->renderGet();
- Resource::response(Resource::STATUS_UNAUTHORIZED, $view);
+ $errors = $this->userService->getErrors();
+ $view = new LoginView($this->getResponse(), $this->getContainer());
+ $view = $view->renderGet($errors);
+ return $this->response(Controller::STATUS_UNAUTHORIZED, $view);
}
}
public function options()
{
//Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,GET'));
+ return $this->response(Controller::STATUS_OK);
}
/**
@@ -119,20 +113,6 @@ public function getOAuthService()
return $this->oAuthService;
}
- /**
- * Sets the value of oAuthService.
- *
- * @param \API\Service\Auth\OAuth $oAuthService the o auth service
- *
- * @return self
- */
- public function setOAuthService(\API\Service\Auth\OAuth $oAuthService)
- {
- $this->oAuthService = $oAuthService;
-
- return $this;
- }
-
/**
* Gets the value of userService.
*
@@ -142,18 +122,4 @@ public function getUserService()
{
return $this->userService;
}
-
- /**
- * Sets the value of userService.
- *
- * @param \API\Service\User $userService the user service
- *
- * @return self
- */
- public function setUserService(\API\Service\User $userService)
- {
- $this->userService = $userService;
-
- return $this;
- }
}
diff --git a/src/xAPI/Resource/V10/Oauth/Token.php b/src/xAPI/Controller/V10/Oauth/Token.php
similarity index 57%
rename from src/xAPI/Resource/V10/Oauth/Token.php
rename to src/xAPI/Controller/V10/Oauth/Token.php
index 6e113f9a..e55d165a 100644
--- a/src/xAPI/Resource/V10/Oauth/Token.php
+++ b/src/xAPI/Controller/V10/Oauth/Token.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,13 +22,13 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10\Oauth;
+namespace API\Controller\V10\Oauth;
-use API\Resource;
+use API\Controller;
use API\Service\Auth\OAuth as OAuthService;
use API\View\V10\OAuth\AccessToken as AccessTokenView;
-class Token extends Resource
+class Token extends Controller
{
/**
* @var \API\Service\Auth\OAuth
@@ -40,29 +40,25 @@ class Token extends Resource
*/
public function init()
{
- $this->setOAuthService(new OAuthService($this->getSlim()));
+ $this->oAuthService = new OAuthService($this->getContainer());
}
public function post()
{
- $request = $this->getSlim()->request();
+ // TODO 0.11.x request validation
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- $this->oAuthService->accessTokenPost($request);
+ $accessTokenDocument = $this->oAuthService->accessTokenPost();
// Authorization is always requested
- $view = new AccessTokenView(['service' => $this->oAuthService]);
- $view = $view->renderGet();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
+ $view = new AccessTokenView($this->getResponse(), $this->getContainer());
+ $view = $view->render($accessTokenDocument);
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
}
public function options()
{
//Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST');
- Resource::response(Resource::STATUS_OK);
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST'));
+ return $this->response(Controller::STATUS_OK);
}
/**
@@ -74,18 +70,4 @@ public function getOAuthService()
{
return $this->oAuthService;
}
-
- /**
- * Sets the value of oAuthService.
- *
- * @param \API\Service\Auth\OAuth $oAuthService the o auth service
- *
- * @return self
- */
- public function setOAuthService(\API\Service\Auth\OAuth $oAuthService)
- {
- $this->oAuthService = $oAuthService;
-
- return $this;
- }
}
diff --git a/src/xAPI/Controller/V10/Statements.php b/src/xAPI/Controller/V10/Statements.php
new file mode 100644
index 00000000..7227d1b3
--- /dev/null
+++ b/src/xAPI/Controller/V10/Statements.php
@@ -0,0 +1,122 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Controller\V10;
+
+use API\Controller;
+use API\Service\Statement as StatementService;
+use API\Validator\V10\Statement as StatementValidator;
+use API\View\V10\Statements as StatementView;
+
+class Statements extends Controller
+{
+ /**
+ * @var \API\Service\Statement
+ */
+ private $statementService;
+
+ /**
+ * @var \API\Validator\Statement
+ */
+ private $statementValidator;
+
+ /**
+ * Get statement service.
+ */
+ public function init()
+ {
+ $this->statementService = new StatementService($this->getContainer());
+ $this->statementValidator = new StatementValidator($this->getContainer());
+ }
+
+ /**
+ * Handle the Statement GET request.
+ */
+ public function get()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('statements/read');
+ $this->getContainer()->get('auth')->requirePermission('statements/read/mine');
+
+ // Do the validation
+ $this->statementValidator->validateRequest();
+ $this->statementValidator->validateGetRequest();
+
+ // Load the statements
+ $statementResult = $this->statementService->statementGet();
+
+ // Render them
+ $view = new StatementView($this->getResponse(), $this->getContainer());
+
+ if ($statementResult->getSingleStatementRequest()) {
+ $view = $view->renderGetSingle($statementResult);
+ } else {
+ $view = $view->renderGet($statementResult);
+ }
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ public function put()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('statements/write');
+
+ $request = $this->getContainer()->get('parser')->getData();
+ // Do the validation
+ $this->statementValidator->validateRequest();
+ $this->statementValidator->validatePutRequest();
+
+ // Save the statements
+ $this->statementService->statementPut();
+
+ // Always an empty response, unless there was an Exception
+ return $this->response(Controller::STATUS_NO_CONTENT);
+ }
+
+ public function post()
+ {
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('statements/write');
+
+ // Do the validation and multipart splitting
+ $this->statementValidator->validateRequest();
+ $this->statementValidator->validatePostRequest();
+
+ // Save the statements
+ $statementResult = $this->statementService->statementPost();
+
+ $view = new StatementView($this->getResponse(), $this->getContainer());
+ $view = $view->renderPost($statementResult);
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ public function options()
+ {
+ //Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,PUT,GET,DELETE'));
+ return $this->response(Controller::STATUS_OK);
+ }
+}
diff --git a/src/xAPI/Util/MongoClient.php b/src/xAPI/ControllerInterface.php
similarity index 63%
rename from src/xAPI/Util/MongoClient.php
rename to src/xAPI/ControllerInterface.php
index f5f9f60f..9d280bbd 100644
--- a/src/xAPI/Util/MongoClient.php
+++ b/src/xAPI/ControllerInterface.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,26 +22,37 @@
* file that was distributed with this source code.
*/
-namespace API\Util;
+namespace API;
-use \Sokil\Mongo\Client;
-use API\Resource;
-
-class MongoClient
+interface ControllerInterface
{
/**
- * @var \Sokil\Mongo\Client $client mongo client instance
+ * Initializes Controller
+ */
+ public function init();
+
+ /**
+ * Http GET callback
+ */
+ public function get();
+
+ /**
+ * Http POST callback
+ */
+ public function post();
+
+ /**
+ * Http PUT callback
*/
- public $client;
+ public function put();
/**
- * @var \Sokil\Mongo\Database $database mongo database instance
+ * Http DELETE callback
*/
- public $db;
+ public function delete();
- public function __construct($config)
- {
- $this->client = new Client($config['database']['host_uri']);
- $this->db = $this->client->getDatabase($config['database']['db_name']);
- }
+ /**
+ * Http OPTIONS callback
+ */
+ public function options();
}
diff --git a/src/xAPI/Document.php b/src/xAPI/Document.php
new file mode 100644
index 00000000..2c91575c
--- /dev/null
+++ b/src/xAPI/Document.php
@@ -0,0 +1,179 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+// TODO 0.11.x: Remove magic getters and setters (strictness)
+
+abstract class Document implements DocumentInterface
+{
+ protected $data;
+
+ protected $state;
+
+ protected $version;
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct($data = null, $documentState = DocumentState::TRUSTED, $version = null)
+ {
+ if (null === $data) {
+ $data = new \stdClass;
+ }
+ $this->data = $data;
+ $this->state = $documentState;
+ $this->version = $version;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getVersion()
+ {
+ return $this->version;
+ }
+
+ /**
+ * Sets the value of xAPI data.
+ *
+ * @param array $data the data
+ *
+ * @return self
+ */
+ protected function setData($data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Sets the value of state.
+ *
+ * @param string $state i/o state of the document
+ *
+ * @return self
+ */
+ protected function setState($state)
+ {
+ $this->state = $state;
+
+ return $this;
+ }
+
+ /**
+ * Sets the value of version.
+ *
+ * @param mixed $version the version
+ *
+ * @return self
+ */
+ protected function setVersion($version)
+ {
+ $this->version = $version;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of a specfier xAPI data property
+ *
+ * @param string $key
+ * @return mixed property value
+ */
+ public function get($key)
+ {
+ if (isset($this->data->{"$key"})) {
+ return $this->data->{"$key"};
+ }
+ }
+
+ /**
+ * Get the value of a specfier xAPI data property
+ *
+ * @param string $key
+ * @return mixed property value
+ */
+ public function set($key, $value)
+ {
+ $this->data->{"$key"} = $value;
+ }
+
+ /**
+ * Handle getters and setters
+ * @param string $name
+ * @param array $arguments
+ * @return mixed
+ */
+ // TODO 0.11.x: Most likely remove the magic
+ public function __call($name, $arguments)
+ {
+ // Getter
+ if ('get' === strtolower(substr($name, 0, 3))) {
+ return $this->get(lcfirst(substr($name, 3)));
+ }
+
+ // Setter
+ if ('set' === strtolower(substr($name, 0, 3)) && isset($arguments[0])) {
+ return $this->set(lcfirst(substr($name, 3)), $arguments[0]);
+ }
+
+ throw new \Exception('Document has no method "' . $name . '"');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function jsonSerialize()
+ {
+ return $this->getData();
+ }
+
+ /**
+ * Get stored xAPI data as array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->getData();
+ }
+}
diff --git a/src/xAPI/Document/AccessToken.php b/src/xAPI/Document/AccessToken.php
new file mode 100644
index 00000000..29e0ec0b
--- /dev/null
+++ b/src/xAPI/Document/AccessToken.php
@@ -0,0 +1,127 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * Projected Usage
+ *
+ * POST/PUT:
+ * $document = new \API\Document\Statement($parsedJson, 'UNTRUSTED', '1.0.3');
+ * $statement = $document->validate()->normalize()->document(); // validated and normalized stdClass, ready for storage, changes the state with each chain ['UNTRUSTED->VALIDTED->READY]
+ *
+ * REST response
+ * $document = new \API\Document\Statement($mongoDocument, 'TRUSTED', '1.0.3');
+ * $document->validate()->normalize(); //deals with minor incositencies, will in future also remove meta properties
+ * $json = json_encode($document);
+ *
+ * $document will have convenience methods and reveal the convenience methods of subproperties
+ * $document->isReferencing();
+ * $document->actor->isAgent();
+ * $document->object->isSubStatement();
+ *
+ * etc..
+ */
+
+namespace API\Document;
+
+use API\Bootstrap;
+use API\Document;
+use API\Controller;
+
+// TODO 0.11.x: implement normalize, validate, etc. (GraphQL)
+
+class AccessToken extends Document
+{
+
+ ////
+ // Setters for new documents
+ ////
+
+ /**
+ * Sets document property: name
+ * @param string|null $name
+ */
+ public function setName($name)
+ {
+ $this->data->name = $name;
+ }
+
+ /**
+ * Sets document property: description
+ * @param string|null $description
+ */
+ public function setDescription($description)
+ {
+ $this->data->description = $description;
+ }
+
+ /**
+ * Sets document property: expiresIn
+ * @param int $expiresIn
+ */
+ public function setExpiresIn($expiresIn)
+ {
+ $until = \API\Util\Date::dateFromSeconds($expiresIn);
+ $until = \API\Util\Date::dateStringToMongoDate($until);
+ $this->setExpiresAt($until);
+ }
+
+ ////
+ // Getters for stored documents
+ ////
+
+ /**
+ * Gets document property: expiresIn
+ * @return int period in seconds
+ */
+ public function getExpiresIn()
+ {
+ $dateTime = new \DateTime();
+ if ($this->getExpiresAt() === null) {
+ return null;
+ } else {
+ $dateTime->setTimestamp($this->getExpiresAt()->sec);
+ $until = \API\Util\Date::secondsUntil($dateTime);
+
+ return $until;
+ }
+ }
+
+ ////
+ // Checks/Validaton for stored documents
+ ////
+
+ /**
+ * Check if fetched token document is expired
+ * @return bool
+ */
+ public function isExpired()
+ {
+ if ($this->getExpired()) {
+ return true;
+ } elseif (null !== $this->getExpiresIn() && $this->getExpiresIn() <= 0) {
+ $this->setExpired(true);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/xAPI/Document/Attachment.php b/src/xAPI/Document/Attachment.php
deleted file mode 100644
index 17850327..00000000
--- a/src/xAPI/Document/Attachment.php
+++ /dev/null
@@ -1,66 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document;
-
-use Sokil\Mongo\Document;
-
-class Attachment extends Document
-{
- protected $_data = [
- 'sha2' => null,
- 'content_type' => null,
- 'mongo_timestamp' => null,
- ];
-
- public function setSha2($sha2)
- {
- $this->_data['sha2'] = $sha2;
- }
-
- public function getSha2()
- {
- return $this->_data['sha2'];
- }
-
- public function setContentType($contentType)
- {
- $this->_data['content_type'] = $contentType;
- }
-
- public function getContentType()
- {
- return $this->_data['content_type'];
- }
-
- public function setTimestamp($timestamp)
- {
- $this->_data['mongo_timestamp'] = $timestamp;
- }
-
- public function getTimestamp()
- {
- return $this->_data['mongo_timestamp'];
- }
-}
diff --git a/src/xAPI/Document/Auth/AbstractToken.php b/src/xAPI/Document/Auth/AbstractToken.php
deleted file mode 100644
index a842b182..00000000
--- a/src/xAPI/Document/Auth/AbstractToken.php
+++ /dev/null
@@ -1,114 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document\Auth;
-
-use Sokil\Mongo\Document;
-use API\Resource;
-
-abstract class AbstractToken extends Document implements \JsonSerializable, TokenInterface
-{
- public function addScope(Scope $scope)
- {
- $this->addRelation('scopes', $scope);
- }
-
- public function isSuperToken()
- {
- return $this->hasPermission('super');
- }
-
- public function hasPermission($permissionName)
- {
- foreach ($this->scopes as $scope) {
- if ($scope->getName() === $permissionName || $scope->getName() === 'super') {
- return true;
- }
-
- if ($permissionName !== 'super' && $scope->getName() === 'all') {
- return true;
- }
- }
-
- return false;
- }
-
- public function checkPermission($permissionName)
- {
- $result = false;
- if (is_array($permissionName)) {
- foreach ($permissionName as $individualPermissionName) {
- if ($this->hasPermission($individualPermissionName)) {
- $result = true;
- }
- }
- } else {
- $result = $this->hasPermission($permissionName);
- }
-
- if ($result) {
- return true;
- } else {
- throw new \Exception('Permission denied.', Resource::STATUS_FORBIDDEN);
- }
- }
-
- public function getExpiresIn()
- {
- $dateTime = new \DateTime();
- if ($this->getExpiresAt() === null) {
- return null;
- } else {
- $dateTime->setTimestamp($this->getExpiresAt()->sec);
- $until = \API\Util\Date::secondsUntil($dateTime);
- return $until;
- }
- }
-
- public function setExpiresIn($expiresIn)
- {
- $until = \API\Util\Date::dateFromSeconds($expiresIn);
- $until = \API\Util\Date::dateStringToMongoDate($until);
- $this->setExpiresAt($until);
-
- return $this;
- }
-
- public function isExpired()
- {
- if ($this->getExpired()) {
- return true;
- } else if (null !== $this->getExpiresIn() && $this->getExpiresIn() <= 0) {
- $this->setExpired(true);
- return true;
- } else {
- return false;
- }
- }
-
- public function jsonSerialize()
- {
- return $this->_data;
- }
-}
diff --git a/src/xAPI/Document/Auth/BasicToken.php b/src/xAPI/Document/Auth/BasicToken.php
deleted file mode 100644
index 05a1030a..00000000
--- a/src/xAPI/Document/Auth/BasicToken.php
+++ /dev/null
@@ -1,63 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document\Auth;
-
-use Slim\Slim;
-
-class BasicToken extends AbstractToken
-{
- protected $_data = [
- 'userId' => null,
- 'key' => null,
- 'secret' => null,
- 'expiresAt' => null,
- 'createdAt' => null,
- ];
-
- public function relations()
- {
- return [
- 'user' => [self::RELATION_BELONGS, 'users', 'userId'],
- 'scopes' => [self::RELATION_MANY_MANY, 'authScopes', 'scopeIds', true],
- 'logs' => [self::RELATION_HAS_MANY, 'logs', 'basicTokenId']
- ];
- }
-
- public function generateAuthority()
- {
- $slim = Slim::getInstance();
- $url = $slim->url;
- $host = $url->getBaseUrl();
- $authority = [
- 'objectType' => 'Agent',
- 'account' => [
- 'homePage' => $host,
- 'name' => $this->user->getEmail(),
- ],
- ];
-
- return $authority;
- }
-}
diff --git a/src/xAPI/Document/Auth/OAuthToken.php b/src/xAPI/Document/Auth/OAuthToken.php
deleted file mode 100644
index 4faa3438..00000000
--- a/src/xAPI/Document/Auth/OAuthToken.php
+++ /dev/null
@@ -1,72 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document\Auth;
-
-use Slim\Slim;
-
-class OAuthToken extends AbstractToken
-{
- protected $_data = [
- 'userId' => null,
- 'clientId' => null,
- 'token' => null,
- 'expiresAt' => null,
- 'createdAt' => null,
- 'code' => null,
- ];
-
- public function relations()
- {
- return [
- 'user' => [self::RELATION_BELONGS, 'users', 'userId'],
- 'client' => [self::RELATION_BELONGS, 'oAuthClients', 'clientId'],
- 'scopes' => [self::RELATION_MANY_MANY, 'authScopes', 'scopeIds', true],
- 'logs' => [self::RELATION_HAS_MANY, 'logs', 'oAuthTokenId']
- ];
- }
-
- public function generateAuthority()
- {
- $slim = Slim::getInstance();
- $url = $slim->url;
- $host = $url->getBaseUrl();
- $authority = [
- 'objectType' => 'Group',
- 'member' => [
- [
- 'account' => [
- 'homePage' => $host.'/oauth/token',
- 'name' => 'oauth_consumer_'.$this->getClientId(),
- ],
- ],
- [
- 'mbox' => 'mailto:'.$this->user->getEmail(),
- ],
- ],
- ];
-
- return $authority;
- }
-}
diff --git a/src/xAPI/Document/Auth/Scope.php b/src/xAPI/Document/Auth/Scope.php
deleted file mode 100644
index ba04a957..00000000
--- a/src/xAPI/Document/Auth/Scope.php
+++ /dev/null
@@ -1,51 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document\Auth;
-
-use Sokil\Mongo\Document;
-
-class Scope extends Document implements \JsonSerializable
-{
- protected $_data = [
- 'name' => null,
- 'description' => null,
- ];
-
- public function relations()
- {
- return [
- 'basicTokens' => [self::RELATION_MANY_MANY, 'basicTokens', 'scopeIds'],
- 'oAuthTokens' => [self::RELATION_MANY_MANY, 'oAuthTokens', 'scopeIds'],
- 'users' => [self::RELATION_MANY_MANY, 'users', 'permissionIds'],
- ];
- }
-
- public function jsonSerialize()
- {
- $return = ['name' => $this->_data['name'], 'description' => $this->_data['description']];
-
- return $return;
- }
-}
diff --git a/src/xAPI/Document/ActivityProfile.php b/src/xAPI/Document/BasicToken.php
similarity index 68%
rename from src/xAPI/Document/ActivityProfile.php
rename to src/xAPI/Document/BasicToken.php
index 0c01585e..2b659835 100644
--- a/src/xAPI/Document/ActivityProfile.php
+++ b/src/xAPI/Document/BasicToken.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,12 +24,19 @@
namespace API\Document;
-use Sokil\Mongo\Document;
-
-class ActivityProfile extends Document
+class BasicToken extends AccessToken
{
- public function getIdentifier()
+ public function generateAuthority()
{
- return $this->getProfileId();
+ $host = $this->getHost();
+ $authority = (object)[
+ 'objectType' => 'Agent',
+ 'account' => [
+ 'homePage' => $host,
+ 'name' => $this->getUser()->email,
+ ],
+ ];
+
+ return $authority;
}
}
diff --git a/src/xAPI/Document/Generic.php b/src/xAPI/Document/Generic.php
new file mode 100644
index 00000000..dcd973dc
--- /dev/null
+++ b/src/xAPI/Document/Generic.php
@@ -0,0 +1,52 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * Projected Usage
+ *
+ * POST/PUT:
+ * $document = new \API\Document\Statement($parsedJson, 'UNTRUSTED', '1.0.3');
+ * $statement = $document->validate()->normalize()->document(); // validated and normalized stdClass, ready for storage, changes the state with each chain ['UNTRUSTED->VALIDTED->READY]
+ *
+ * REST response
+ * $document = new \API\Document\Statement($mongoDocument, 'TRUSTED', '1.0.3');
+ * $document->validate()->normalize(); //deals with minor incositencies, will in future also remove meta properties
+ * $json = json_encode($document);
+ *
+ * $document will have convenience methods and reveal the convenience methods of subproperties
+ * $document->isReferencing();
+ * $document->actor->isAgent();
+ * $document->object->isSubStatement();
+ *
+ * etc..
+ */
+
+namespace API\Document;
+
+use API\Document;
+
+// TODO 0.11.x: Define interface for normalize, validate, etc. (GraphQL)
+
+
+class Generic extends Document
+{
+}
diff --git a/src/xAPI/Document/Log.php b/src/xAPI/Document/Log.php
deleted file mode 100644
index 8659c955..00000000
--- a/src/xAPI/Document/Log.php
+++ /dev/null
@@ -1,51 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document;
-
-use Sokil\Mongo\Document;
-
-class Log extends Document
-{
- protected $_data = [
- 'ip' => null,
- 'method' => null,
- 'endpoint' => null,
- 'timestamp' => null,
- 'basicTokenId' => null,
- 'oAuthTokenId' => null
- ];
-
- public function relations()
- {
- return [
- 'basicToken' => [self::RELATION_BELONGS, 'basicTokens', 'basicTokenId'],
- 'oAuthToken' => [self::RELATION_BELONGS, 'oAuthTokens', 'oAuthTokenId'],
- 'statements' => [self::RELATION_HAS_MANY, 'statements', 'logId'],
- 'activityProfiles' => [self::RELATION_HAS_MANY, 'activityProfiles', 'logId'],
- 'activityStates' => [self::RELATION_HAS_MANY, 'activityStates', 'logId'],
- 'agentProfiles' => [self::RELATION_HAS_MANY, 'agentProfiles', 'logId']
- ];
- }
-}
diff --git a/src/xAPI/Document/OAuthToken.php b/src/xAPI/Document/OAuthToken.php
new file mode 100644
index 00000000..371b939d
--- /dev/null
+++ b/src/xAPI/Document/OAuthToken.php
@@ -0,0 +1,49 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Document;
+
+class OAuthToken extends AccessToken
+{
+ public function generateAuthority()
+ {
+ $host = $this->getHost();
+ $authority = [
+ 'objectType' => 'Group',
+ 'member' => (object)[
+ [
+ 'account' => (object)[
+ 'homePage' => $host.'/oauth/token',
+ 'name' => 'oauth_consumer_'.$this->getClientId(),
+ ],
+ ],
+ [
+ 'mbox' => 'mailto:'.$this->getUser()->email,
+ ],
+ ],
+ ];
+
+ return $authority;
+ }
+}
diff --git a/src/xAPI/Document/Statement.php b/src/xAPI/Document/Statement.php
index b4623c52..ec88c815 100644
--- a/src/xAPI/Document/Statement.php
+++ b/src/xAPI/Document/Statement.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2016 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,86 +20,202 @@
*
* For authorship information, please view the AUTHORS
* file that was distributed with this source code.
+ *
+ * Projected Usage
+ *
+ * POST/PUT:
+ * $document = new \API\Document\Statement($parsedJson, 'UNTRUSTED', '1.0.3');
+ * $statement = $document->validate()->normalize()->document(); // validated and normalized stdClass, ready for storage, changes the state with each chain ['UNTRUSTED->VALIDTED->READY]
+ *
+ * REST response
+ * $document = new \API\Document\Statement($mongoDocument, 'TRUSTED', '1.0.3');
+ * $document->validate()->normalize(); //deals with minor incositencies, will in future also remove meta properties
+ * $json = json_encode($document);
+ *
+ * $document will have convenience methods and reveal the convenience methods of subproperties
+ * $document->isReferencing();
+ * $document->actor->isAgent();
+ * $document->object->isSubStatement();
+ *
+ * etc..
*/
namespace API\Document;
-use Sokil\Mongo\Document;
-use JsonSerializable;
-use Rhumsaa\Uuid\Uuid;
+use Ramsey\Uuid\Uuid;
use League\Url\Url;
-use API\Resource;
+use API\Controller;
+use API\Document;
+use API\DocumentState;
+use API\Util;
+
+// TODO 0.11.x: implement normalize, validate, etc. (GraphQL)
-class Statement extends Document implements JsonSerializable
+class Statement extends Document
{
- protected $_data = [
- 'statement' => [
- 'authority' => null,
- 'id' => null,
- 'actor' => null,
- 'verb' => null,
- 'object' => null,
- 'timestamp' => null,
- 'stored' => null,
- ],
- 'mongo_timestamp' => null,
- 'voided' => false,
- 'logId' => null
- ];
-
- public function setStatement($statement)
+ public static function fromDatabase($document)
+ {
+ $documentState = DocumentState::TRUSTED;
+ $version = $document->version;
+ $statement = new self($document, $documentState, $version);
+ return $statement;
+ }
+
+ public static function fromApi($document, $version)
+ {
+ $documentState = DocumentState::UNTRUSTED;
+ $data = (object)[];
+ $data->statement = $document;
+ $statement = new self($data, $documentState, $version);
+ return $statement;
+ }
+
+ public function validate()
+ {
+ // required check props, additional props: basic actor, object, result
+ /*$validator = new Validator\Statement($this->document->actor, $this->state, $this->version);
+ $validator->validate($this->document, $this->mode); //throws Exceptions
+
+ $this->actor = new Actor($this->document->actor, $this->state, $this->version);
+ $this->document->actor = $this->actor->document();
+
+ $this->verb = new Verb($this->document->result, $this->state, $this->version);
+ $this->document->verb = $this->verb->document();
+
+ $this->object = new Object_($this->document->object, $this->state, $this->version);
+ $this->document->object = $this->object->document();
+
+ // optional props
+ if(isset($this->document->result)){
+ $this->result = new Result($this->document->result, $this->state, $this->version);
+ $this->document->result = $this->result->document();
+ }
+
+ return $this;*/
+ }
+
+ public function normalize()
+ {
+ // Actually there is something to do here - add metadata!
+ // For example, adding the mongo_timestamp
+ // Maybe also adding the version in a version key!
+
+ // nothing to do here sub modules take care
+ // @Joerg: What are submodules?
+ return $this;
+ }
+
+ /*public function get($key)
+ {
+ if (isset($this->data['statement'][$key])) {
+ return $this->data['statement'][$key];
+ }
+ }
+
+ public function set($key, $value)
{
- $this->_data['statement'] = $statement;
+ $this->data['statement'][$key] = $value;
}
public function getStatement()
{
- return $this->_data['statement'];
+ if (isset($this->data['statement->)) {
+ return $this->data['statement->;
+ }
+ }
+
+ public function getMetadata()
+ {
+ if (isset($this->data['metadata->)) {
+ return $this->data['metadata->;
+ }
+ }*/
+
+ public function getId()
+ {
+ return $this->data->{'_id'};
}
public function setStored($timestamp)
{
- $this->_data['statement']['stored'] = $timestamp;
+ $this->data->statement->stored = $timestamp;
}
public function getStored()
{
- return $this->_data['statement']['stored'];
+ return $this->data->statement->stored;
}
public function setTimestamp($timestamp)
{
- $this->_data['statement']['timestamp'] = $timestamp;
+ $this->data->statement->timestamp = $timestamp;
}
public function getTimestamp()
{
- return $this->_data['statement']['timestamp'];
+ return $this->data->statement->timestamp;
}
public function setMongoTimestamp($timestamp)
{
- $this->_data['mongo_timestamp'] = $timestamp;
+ $this->data->mongo_timestamp = $timestamp;
}
public function getMongoTimestamp()
{
- return $this->_data['mongo_timestamp'];
+ return $this->data->mongo_timestamp;
+ }
+
+ public function renderExact()
+ {
+ $this->convertExtensionKeysFromUnicode();
+
+ return $this->data->statement;
+ }
+
+ public function renderMeta()
+ {
+ return $this->data->statement->id;
+ }
+
+ public function renderCanonical()
+ {
+ throw new \InvalidArgumentException('The \'canonical\' statement format is currently not supported.', Controller::STATUS_NOT_IMPLEMENTED);
}
public function setDefaultTimestamp()
{
- if (!isset($this->_data['statement']['timestamp']) || null === $this->_data['statement']['timestamp']) {
- $this->_data['statement']['timestamp'] = $this->_data['statement']['stored'];
+ if (!isset($this->data->statement->timestamp) || null === $this->data->statement->timestamp) {
+ $this->data->statement->timestamp = $this->data->statement->stored;
+ }
+ }
+
+ /**
+ * Mutate legacy statement.context.contextActivities
+ * wraps single activity object (per type) into an array.
+ */
+ public function legacyContextActivities()
+ {
+ if (!isset($this->data->statement->context)) {
+ return;
+ }
+ if (!isset($this->data->statement->context->contextActivities)) {
+ return;
+ }
+ foreach ($this->data->statement->context->contextActivities as $type => $value) {
+ // We are a bit rat-trapped because statement is an associative array, most efficient way to check if numeric array is here to check for required 'id' property
+ if (isset($value->id)) {
+ $this->data->statement->context->contextActivities->{$type} = [$value];
+ }
}
}
public function isVoiding()
{
- if (isset($this->_data['statement']['verb']['id'])
- && ($this->_data['statement']['verb']['id'] === 'http://adlnet.gov/expapi/verbs/voided')
- && isset($this->_data['statement']['object']['objectType'])
- && ($this->_data['statement']['object']['objectType'] === 'StatementRef')
+ if (isset($this->data->statement->verb->id)
+ && ($this->data->statement->verb->id === 'http://adlnet.gov/expapi/verbs/voided')
+ && isset($this->data->statement->object->objectType)
+ && ($this->data->statement->object->objectType === 'StatementRef')
) {
return true;
} else {
@@ -109,39 +225,32 @@ public function isVoiding()
public function isReferencing()
{
- if (isset($this->_data['statement']['object']['objectType'])
- && ($this->_data['statement']['object']['objectType'] === 'StatementRef'))
- {
+ if (isset($this->data->statement->object->objectType)
+ && ($this->data->statement->object->objectType === 'StatementRef')) {
return true;
} else {
return false;
}
}
- public function getReferencedStatement()
+ public function getReferencedStatementId()
{
- $referencedId = $this->_data['statement']['object']['id'];
-
- $referencedStatement = $this->getCollection()->find()->where('statement.id', $referencedId)->current();
+ $referencedId = $this->data->statement->object->id;
- if (null === $referencedStatement) {
- throw new \InvalidArgumentException('Referenced statement does not exist!', Resource::STATUS_BAD_REQUEST);
- }
-
- return $referencedStatement;
+ return $referencedId;
}
public function fixAttachmentLinks($baseUrl)
{
- if (isset($this->_data['statement']['attachments'])) {
- if(!is_array($this->_data['statement']['attachments'])){
+ if (isset($this->data->statement->attachments)) {
+ if (!is_array($this->data->statement->attachments)) {
return;
}
- foreach ($this->_data['statement']['attachments'] as &$attachment) {
- if (!isset($attachment['fileUrl'])) {
+ foreach ($this->data->statement->attachments as &$attachment) {
+ if (!isset($attachment->fileUrl)) {
$url = Url::createFromUrl($baseUrl);
- $url->getQuery()->modify(['sha2' => $attachment['sha2']]);
- $attachment['fileUrl'] = $url->__toString();
+ $url->getQuery()->modify(['sha2' => $attachment->sha2]);
+ $attachment->fileUrl = $url->__toString();
}
}
}
@@ -149,100 +258,144 @@ public function fixAttachmentLinks($baseUrl)
public function convertExtensionKeysToUnicode()
{
- if (isset($this->_data['statement']['context']['extensions'])) {
- if(!is_array($this->_data['statement']['context']['extensions'])){
- return;
- }
- foreach ($this->_data['statement']['context']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('.', '\uFF0E', $extensionKey);
- $this->_data['statement']['context']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['context']['extensions'][$extensionKey]);
+ if (isset($this->data->statement->context->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->context->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('.', '\uFF0E', $oldExtensionKey);
+ $this->data->statement->context->extensions->{$newExtensionKey} = $this->data->statement->context->extensions->{$oldExtensionKey};
+ unset($this->data->statement->context->extensions->{$oldExtensionKey});
}
}
- if (isset($this->_data['statement']['result']['extensions'])) {
- if(!is_array($this->_data['statement']['result']['extensions'])){
- return;
- }
- foreach ($this->_data['statement']['result']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('.', '\uFF0E', $extensionKey);
- $this->_data['statement']['result']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['result']['extensions'][$extensionKey]);
+ if (isset($this->data->statement->result->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->result->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('.', '\uFF0E', $oldExtensionKey);
+ $this->data->statement->result->extensions->{$newExtensionKey} = $this->data->statement->result->extensions->{$oldExtensionKey};
+ unset($this->data->statement->result->extensions->{$oldExtensionKey});
}
}
- if (isset($this->_data['statement']['object']['definition']['extensions'])) {
- if(!is_array($this->_data['statement']['object']['definition']['extensions'])){
- return;
- }
- foreach ($this->_data['statement']['object']['definition']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('.', '\uFF0E', $extensionKey);
- $this->_data['statement']['object']['definition']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['object']['definition']['extensions'][$extensionKey]);
+ if (isset($this->data->statement->object->definition->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->object->definition->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('.', '\uFF0E', $oldExtensionKey);
+ $this->data->statement->object->definition->extensions->{$newExtensionKey} = $this->data->statement->object->definition->extensions->{$oldExtensionKey};
+ unset($this->data->statement->object->definition->extensions->{$oldExtensionKey});
}
}
}
+ public function normalizeExistingIds()
+ {
+ if (!empty($this->data->statement->id) && $this->data->statement->id !== null) {
+ $this->data->statement->id = Util\xAPI::normalizeUuid($this->data->statement->id);
+ }
+
+ if ($this->isReferencing()) {
+ $this->data->statement->object->id = Util\xAPI::normalizeUuid($this->data->statement->object->id);
+ }
+
+ if (!empty($this->data->statement->context->registration) && $this->data->statement->context->registration !== null) {
+ $this->data->statement->context->registration = Util\xAPI::normalizeUuid($this->data->statement->context->registration);
+ }
+ }
+
+ public function setDefaultId()
+ {
+ // If no ID has been set, set it
+ if (empty($this->data->statement->id) || $this->data->statement->id === null) {
+ $this->data->statement->id = Uuid::uuid4()->toString();
+ }
+ }
+
public function convertExtensionKeysFromUnicode()
{
- if (isset($this->_data['statement']['context']['extensions'])) {
- if(!is_array($this->_data['statement']['context']['extensions'])){
- return;
- }
- foreach ($this->_data['statement']['context']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('\uFF0E', '.', $extensionKey);
- $this->_data['statement']['context']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['context']['extensions'][$extensionKey]);
+ if (isset($this->data->statement->context->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->context->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('\uFF0E', '.', $oldExtensionKey);
+ $this->data->statement->context->extensions->{$newExtensionKey} = $this->data->statement->context->extensions->{$oldExtensionKey};
+ unset($this->data->statement->context->extensions->{$oldExtensionKey});
}
}
- if (isset($this->_data['statement']['result']['extensions'])) {
- if(!is_array($this->_data['statement']['result']['extensions'])){
- return;
- }
- foreach ($this->_data['statement']['result']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('\uFF0E', '.', $extensionKey);
- $this->_data['statement']['result']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['result']['extensions'][$extensionKey]);
+ if (isset($this->data->statement->result->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->result->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('\uFF0E', '.', $oldExtensionKey);
+ $this->data->statement->result->extensions->{$newExtensionKey} = $this->data->statement->result->extensions->{$oldExtensionKey};
+ unset($this->data->statement->result->extensions->{$oldExtensionKey});
}
}
- if (isset($this->_data['statement']['object']['definition']['extensions'])) {
- if(!is_array($this->_data['statement']['object']['definition']['extensions'])){
- return;
+ if (isset($this->data->statement->object->definition->extensions)) {
+ $oldExtensionKeys = array_keys(get_object_vars($this->data->statement->object->definition->extensions));
+ foreach ($oldExtensionKeys as $oldExtensionKey) {
+ $newExtensionKey = str_replace('\uFF0E', '.', $oldExtensionKey);
+ $this->data->statement->object->definition->extensions->{$newExtensionKey} = $this->data->statement->object->definition->extensions->{$oldExtensionKey};
+ unset($this->data->statement->object->definition->extensions->{$oldExtensionKey});
}
- foreach ($this->_data['statement']['object']['definition']['extensions'] as $extensionKey => $extensionValue) {
- $newExtensionKey = str_replace('\uFF0E', '.', $extensionKey);
- $this->_data['statement']['object']['definition']['extensions'][$newExtensionKey] = $extensionValue;
- unset($this->_data['statement']['object']['definition']['extensions'][$extensionKey]);
+ }
+ }
+
+ public function renderIds()
+ {
+ $this->convertExtensionKeysFromUnicode();
+ $statement = $this->data->statement;
+
+ if (isset($statement->actor->objectType) && $statement->actor->objectType === 'Group') {
+ $statement->actor->member = array_map(function ($singleMember) {
+ return $this->simplifyObject($singleMember);
+ }, $statement->actor->member);
+ } else {
+ $statement->actor = $this->simplifyObject($statement->actor);
+ }
+
+ if (isset($statement->object->objectType) && $statement->object->objectType === 'SubStatement') {
+ if ($statement->object->actor->objectType === 'Group') {
+ $statement->object->actor->member = array_map(function ($singleMember) {
+ return $this->simplifyObject($singleMember);
+ }, $statement->object->actor->member);
+ } else {
+ $statement->object->actor = $this->simplifyObject($statement->object->actor);
}
+ $statement->object->object = $this->simplifyObject($statement->object->object);
+ } else {
+ $statement->object = $this->simplifyObject($statement->object);
+ }
+
+ return $statement;
+ }
+
+ private function simplifyObject($object)
+ {
+ if (isset($object->mbox)) {
+ $uniqueIdentifier = 'mbox';
+ } elseif (isset($object->mbox_sha1sum)) {
+ $uniqueIdentifier = 'mbox_sha1sum';
+ } elseif (isset($object->openid)) {
+ $uniqueIdentifier = 'openid';
+ } elseif (isset($object->account)) {
+ $uniqueIdentifier = 'account';
+ } elseif (isset($object->id)) {
+ $uniqueIdentifier = 'id';
}
+ $object = [
+ 'objectType' => $object->objectType,
+ $uniqueIdentifier => $object->{$uniqueIdentifier}
+ ];
+ return $object;
}
public function extractActivities()
{
$activities = [];
// Main activity
- if ((isset($this->_data['statement']['object']['objectType']) && $this->_data['statement']['object']['objectType'] === 'Activity') || !isset($this->_data['statement']['object']['objectType'])) {
- $activity = $this->_data['statement']['object'];
-
- // Sort of a hack - PHP's copy-on-write needs to be executed, otherwise the MongoDB PHP driver
- // overwrites the contents of the variable being passed to the batchInsert call - regardless of
- // whether the variable has been passed by reference or not!
- // See more:
- // http://php.net/manual/en/mongocollection.insert.php Insert behaviour
- // http://php.net/manual/en/mongocollection.batchinsert.php Should behave the same as insert, however, does not
- // https://jira.mongodb.org/browse/PHP-383
- // http://www.phpinternalsbook.com/zvals/memory_management.html#reference-counting-and-copy-on-write
- //
- // TODO: Report bug to Mongo bug tracker
- //
- $activity['DUMMY'] = 'DUMMY';
- unset($activity['DUMMY']);
-
+ if ((isset($this->data->statement->object->objectType) && $this->data->statement->object->objectType === 'Activity') || !isset($this->data->statement->object->objectType)) {
+ $activity = $this->data->statement->object;
$activities[] = $activity;
}
-
/* Commented out for now due to performance reasons
// Context activities
if (isset($this->_data['statement']['context']['contextActivities'])) {
@@ -273,108 +426,6 @@ public function extractActivities()
$activities[] = $this->_data['statement']['object']['object'];
}
}*/
-
return $activities;
}
-
- /**
- * Mutate legacy statement.context.contextActivities
- * wraps single activity object (per type) into an array
- * @return void
- */
- public function legacyContextActivities()
- {
- if (!isset($this->_data['statement']['context'])) {
- return;
- }
- if (!isset($this->_data['statement']['context']['contextActivities'])) {
- return;
- }
- foreach($this->_data['statement']['context']['contextActivities'] as $type => $value){
- // we are a bit rat-trapped because statement is an associative array, most efficient way to check if numeric array is here to check for required 'id' property
- if(isset($value['id'])){
- $this->_data['statement']['context']['contextActivities'][$type] = array($value);
- }
- }
- }
-
- public function jsonSerialize()
- {
- return $this->getStatement();
- }
-
- public function setDefaultId()
- {
- // If no ID has been set, set it
- if (empty($this->_data['statement']['id']) || $this->_data['statement']['id'] === null) {
- $this->_data['statement'] = ['id' => Uuid::uuid4()->toString()] + $this->_data['statement'];
- }
- }
-
- public function renderExact()
- {
- $this->convertExtensionKeysFromUnicode();
- return $this->getStatement();
- }
-
- public function renderMeta()
- {
- return $this->getStatement()['id'];
- }
-
- public function renderCanonical()
- {
- throw new \InvalidArgumentException('The \'canonical\' statement format is currently not supported.', Resource::STATUS_NOT_IMPLEMENTED);
- }
-
- public function renderIds()
- {
- $this->convertExtensionKeysFromUnicode();
- $statement = $this->getStatement();
-
- if ($statement['actor']['objectType'] === 'Group') {
- $statement['actor'] = array_map(function ($singleMember) {
- return $this->simplifyObject($singleMember);
- }, $statement['actor']);
- } else {
- $statement['actor'] = $this->simplifyObject($statement['actor']);
- }
-
- if ($statement['object']['objectType'] !== 'SubStatement') {
- $statement['object'] = $this->simplifyObject($statement['object']);
- } else {
- if ($statement['object']['actor']['objectType'] === 'Group') {
- $statement['object']['actor'] = array_map(function ($singleMember) {
- return $this->simplifyObject($singleMember);
- }, $statement['object']['actor']);
- } else {
- $statement['object']['actor'] = $this->simplifyObject($statement['object']['actor']);
- }
- $statement['object']['object'] = $this->simplifyObject($statement['object']['object']);
- }
-
- return $statement;
- }
-
- private function simplifyObject($object)
- {
- if (isset($object['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($object['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($object['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($object['account'])) {
- $uniqueIdentifier = 'account';
- } elseif (isset($object['id'])) {
- $uniqueIdentifier = 'id';
- }
-
- $object = [
- 'objectType' => $object['objectType'],
- $uniqueIdentifier => $object[$uniqueIdentifier]
- ];
-
- return $object;
- }
}
diff --git a/src/xAPI/Document/User.php b/src/xAPI/Document/User.php
deleted file mode 100644
index 4698629e..00000000
--- a/src/xAPI/Document/User.php
+++ /dev/null
@@ -1,88 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Document;
-
-use Sokil\Mongo\Document;
-use API\Resource;
-use API\Document\Auth\Scope;
-
-class User extends Document implements \JsonSerializable
-{
- protected $_data = [
- 'email' => null,
- 'passwordHash' => null,
- ];
-
- public function relations()
- {
- return [
- 'basicAuthTokens' => [self::RELATION_HAS_MANY, 'basicAuthTokens', 'userId'],
- 'oAuthTokens' => [self::RELATION_HAS_MANY, 'oAuthTokens', 'userId'],
- 'permissions' => [self::RELATION_MANY_MANY, 'authScopes', 'permissionIds', true],
- ];
- }
-
- public function addPermission(Scope $scope)
- {
- $this->addRelation('permissions', $scope);
- }
-
- public function isSuperUser()
- {
- return $this->hasPermission('super');
- }
-
- public function hasPermission($permissionName)
- {
- foreach ($this->permissions as $permission) {
- if ($permission->getName() === $permissionName || $permission->getName() === 'super') {
- return true;
- }
- }
-
- return false;
- }
-
- public function checkPermission($permissionName)
- {
- if ($this->hasPermission($permissionName)) {
- return true;
- } else {
- return new \Exception('Permission denied.', Resource::STATUS_FORBIDDEN);
- }
- }
-
- public function renderSummary()
- {
- $return = ['email' => $this->_data['email'], 'permissions' => array_values($this->permissions)];
-
- return $return;
- }
-
- public function jsonSerialize()
- {
- return $this->_data;
- }
-}
diff --git a/src/xAPI/DocumentInterface.php b/src/xAPI/DocumentInterface.php
new file mode 100644
index 00000000..1bf3cc69
--- /dev/null
+++ b/src/xAPI/DocumentInterface.php
@@ -0,0 +1,66 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API;
+
+interface DocumentInterface extends \JsonSerializable
+{
+ /**
+ * Constructor
+ *
+ * @param array $data xAPI data
+ * @param string $documentState EUNUM string of i/o state of the document (i.e 'TRUSTED', 'UNTRUSTED', etc..)
+ * @param string $version xAPI version
+ * @return void
+ */
+ public function __construct($data = [], $documentState = null, $version = null);
+
+ /**
+ * Get stored data
+ *
+ * @return array xAPI data
+ */
+ public function getData();
+
+ /**
+ * Get stored document state
+ *
+ * @return string i/o state of the document
+ */
+ public function getState();
+
+ /**
+ * Get stored xAPI version
+ *
+ * @return string
+ */
+ public function getVersion();
+
+ /**
+ * Get stored document state
+ *
+ * @return string i/o state of the document
+ */
+ public function toArray();
+}
diff --git a/src/xAPI/DocumentState.php b/src/xAPI/DocumentState.php
new file mode 100644
index 00000000..76d9895a
--- /dev/null
+++ b/src/xAPI/DocumentState.php
@@ -0,0 +1,50 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * Projected Usage
+ *
+ * POST/PUT:
+ * $document = new \API\Document\Statement($parsedJson, 'UNTRUSTED', '1.0.3');
+ * $statement = $document->validate()->normalize()->document(); // validated and normalized stdClass, ready for storage, changes the state with each chain ['UNTRUSTED->VALIDTED->READY]
+ *
+ * REST response
+ * $document = new \API\Document\Statement($mongoDocument, 'TRUSTED', '1.0.3');
+ * $document->validate()->normalize(); //deals with minor incositencies, will in future also remove meta properties
+ * $json = json_encode($document);
+ *
+ * $document will have convenience methods and reveal the convenience methods of subproperties
+ * $document->isReferencing();
+ * $document->actor->isAgent();
+ * $document->object->isSubStatement();
+ *
+ * etc..
+ */
+
+namespace API;
+
+class DocumentState
+{
+ const UNTRUSTED = 0;
+ const TRUSTED = 1;
+ const VALIDATED = 2;
+}
diff --git a/src/xAPI/Extensions/.gitignore b/src/xAPI/Extensions/.gitignore
new file mode 100644
index 00000000..1ea2bbf0
--- /dev/null
+++ b/src/xAPI/Extensions/.gitignore
@@ -0,0 +1,11 @@
+# ignore everything
+
+*
+
+# add interfaces
+!.gitignore
+!ExtensionInterface.php
+!ExtensionException.php
+
+# add core extensions
+!ExtendedQuery
diff --git a/src/xAPI/Extensions/ExtendedQuery/Controller/V10/ExtendedQuery.php b/src/xAPI/Extensions/ExtendedQuery/Controller/V10/ExtendedQuery.php
new file mode 100644
index 00000000..49399423
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/Controller/V10/ExtendedQuery.php
@@ -0,0 +1,120 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery\Controller\V10;
+
+use API\Controller;
+use API\Extensions\ExtendedQuery\Service\Statement as ExtendedStatementService;
+use API\Extensions\ExtendedQuery\View\V10\ProjectedStatement as ProjectedStatementView;
+
+use API\Extensions\ExtensionException as Exception;
+
+/**
+ * Extension Controller class
+ * @see \API\ControllerInterface
+ */
+class ExtendedQuery extends Controller
+{
+ /**
+ * @var API\Extensions\ExtendedQuery\Service\Statement $extendedStatementService Servive instance
+ */
+ private $extendedStatementService;
+
+ /**
+ * Initialize controller
+ * @return void
+ */
+ public function init()
+ {
+ $this->extendedStatementService = new ExtendedStatementService($this->getContainer());
+ }
+
+ /**
+ * Process GET request
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function get()
+ {
+ $request = $this->getRequest();
+
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('ext/extendedquery/statements');
+
+ $documentResult = $this->getExtendedStatementService()->statementGet();
+
+ // Render them
+ $view = new ProjectedStatementView($this->getResponse(), $this->getContainer());
+
+ $view = $view->render($documentResult);
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ /**
+ * Process POST request
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function post()
+ {
+ $request = $this->getRequest();
+
+ // TODO 0.11.x, Move header validation in a json-schema
+ if ($request->getMediaType() !== 'application/json') {
+ throw new Exception('Media type specified in Content-Type header must be \'application/json\'!', Controller::STATUS_BAD_REQUEST);
+ }
+
+ // Check authentication
+ $this->getContainer()->get('auth')->requirePermission('ext/extendedquery/statements');
+
+ // Load the statements - this needs to change, drastically, as it's garbage
+ $documentResult = $this->getExtendedStatementService()->statementPost();
+
+ // Render them
+ $view = new ProjectedStatementView($this->getResponse(), $this->getContainer());
+
+ $view = $view->render($documentResult);
+
+ return $this->jsonResponse(Controller::STATUS_OK, $view);
+ }
+
+ /**
+ * Process OPTIONS request
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function options()
+ {
+ // Handle options request
+ $this->setResponse($this->getResponse()->withHeader('Allow', 'POST,HEAD,GET,OPTIONS'));
+ return $this->response(Controller::STATUS_OK);
+ }
+
+ /**
+ * Get extendedStatementService instance
+ * @return API\Extensions\ExtendedQuery\Service\Statement $extendedStatementService Servive instance
+ */
+ public function getExtendedStatementService()
+ {
+ return $this->extendedStatementService;
+ }
+}
diff --git a/src/xAPI/Extensions/ExtendedQuery/ExtendedQuery.php b/src/xAPI/Extensions/ExtendedQuery/ExtendedQuery.php
new file mode 100644
index 00000000..f3b72ca8
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/ExtendedQuery.php
@@ -0,0 +1,124 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery;
+
+use API\BaseTrait;
+use API\Extensions\ExtensionInterface;
+
+/**
+ * Extended Query extension (Fragmented REST GET queries)
+ * Main class - handles registration and installation of extension
+ */
+class ExtendedQuery implements ExtensionInterface
+{
+ use BaseTrait;
+
+ /**
+ * @var array $routes
+ */
+ private $routes = [
+ '/plus/statements/find' => [
+ 'module' => 'ExtendedQuery',
+ 'methods' => ['GET', 'HEAD', 'POST', 'OPTIONS'],
+ 'description' =>'find statements',
+ 'controller' => 'API\\Extensions\\ExtendedQuery\\Controller\\V10\\ExtendedQuery',
+ ]
+ ];
+
+ /**
+ * constructor
+ * Register services
+ * @param \Psr\Container\ContainerInterface $container
+ */
+ public function __construct($container)
+ {
+ $this->setContainer($container);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function about()
+ {
+ return [
+ 'name' => 'ExtendedQuery',
+ 'description' => 'Fragmented statement queries',
+ 'endpoints' => array_map(function ($route) {
+ return [
+ 'methods' => $route['methods']
+ ];
+ }, $this->routes),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function install()
+ {
+ }
+
+ /**
+ * Returns any event listeners that need to be added for this extension.
+ * @return array Format: [['event' => 'statement.get', 'callable' => function(), 'priority' => 1 (optional)], [], ...]
+ */
+ public function getEventListeners()
+ {
+ return [];
+ }
+
+ /**
+ * Returns any routes that need to be added for this extension.
+ * @return array Format: [['pattern' => '/plus/superstatements', 'callable' => function(), 'methods' => ['GET', 'HEAD']], [], ...]
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+
+ /**
+ * Returns any hooks that need to be added for this extension.
+ * @return array Format: [['hook' => 'slim.before.router', 'callable' => function()], [], ...]
+ */
+ public function getHooks()
+ {
+ return [];
+ }
+
+ /**
+ * Load controller
+ * @param \Psr\Http\Message\ResponseInterface $request
+ * @param \Psr\Http\Message\ResponseInterface $response
+ * @return \API\ControllerInterface
+ */
+ protected function getResource($request, $response)
+ {
+ $versionString = $this->getContainer()->get('version')->generateClassNamespace();
+ $resourceName = __NAMESPACE__.'\\Controller\\'.$versionString.'\\ExtendedQuery';
+ $resource = new $resourceName($this->getContainer(), $request, $response);
+
+ return $resource;
+ }
+}
diff --git a/src/xAPI/Extensions/ExtendedQuery/Service/Statement.php b/src/xAPI/Extensions/ExtendedQuery/Service/Statement.php
new file mode 100644
index 00000000..1c9c6dbd
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/Service/Statement.php
@@ -0,0 +1,114 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery\Service;
+
+use API\Service;
+use API\Controller;
+use Slim\Helper\Set;
+use API\Config;
+
+use API\Extensions\ExtensionException as Exception;
+
+/**
+ * Statements Service
+ */
+class Statement extends Service
+{
+ /**
+ * Fetches statement documents according to the given parameters.
+ * @return \API\Storage\Query\StatementInterface collection of statement documents
+ */
+ public function statementGet()
+ {
+ $parameters = $this->getContainer()->get('parser')->getData()->getParameters();
+
+ $response = $this->statementQuery($parameters);
+
+ return $response;
+ }
+
+ public function statementPost()
+ {
+ // Validation has been completed already - everyhing is assumed to be valid
+ $parameters = $this->getContainer()->get('parser')->getData()->getParameters();
+ $bodyParams = $this->getContainer()->get('parser')->getData()->getPayload();
+
+ $allParams = (object)array_merge((array)$parameters, (array)$bodyParams);
+ $response = $this->statementQuery($allParams);
+
+ return $response;
+ }
+
+ /**
+ * Fetches statement documents according to the given parameters.
+ * @return \API\Storage\Query\StatementInterface collection of statement documents
+ */
+ protected function statementQuery($parameters)
+ {
+ $parameters = (object)$parameters;
+ $storageClass = $this->resolveStorageClass();
+ $extendedStatementStorage = new $storageClass($this->getContainer());
+
+ // Parse parameters
+ if (isset($parameters->query) && is_string($parameters->query)) {
+ $parameters->query = json_decode($parameters->query);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new Exception('Invalid JSON in query param.', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ if (isset($parameters->projection) && is_string($parameters->projection)) {
+ $parameters->projection = json_decode($parameters->projection);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new Exception('Invalid JSON in projection param.', Controller::STATUS_BAD_REQUEST);
+ }
+
+ foreach ($parameters->projection as $field => $value) {
+ if (strpos($field, 'statement.') !== 0) {
+ throw new Exception('Invalid projection parameters!.', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+ }
+
+ $statementResult = $extendedStatementStorage->extendedQuery($parameters);
+
+ return $statementResult;
+ }
+
+ /**
+ * Multiple storage support
+ *
+ * @return string class name
+ */
+ protected function resolveStorageClass()
+ {
+ $storageInUse = Config::get(['storage', 'in_use']);
+ $storageClass = '\\API\\Extensions\\ExtendedQuery\\Storage\\Adapter\\'.$storageInUse.'\\ExtendedStatement';
+ if (!class_exists($storageClass)) {
+ throw new Exception('Storage type selected in config is incompatible with ExtendedQuery extension!', Controller::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ return $storageClass;
+ }
+}
diff --git a/src/xAPI/Extensions/ExtendedQuery/Storage/Adapter/Mongo/ExtendedStatement.php b/src/xAPI/Extensions/ExtendedQuery/Storage/Adapter/Mongo/ExtendedStatement.php
new file mode 100644
index 00000000..b30cada8
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/Storage/Adapter/Mongo/ExtendedStatement.php
@@ -0,0 +1,121 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery\Storage\Adapter\Mongo;
+
+use API\Extensions\ExtendedQuery\Storage\Query\ExtendedStatementInterface;
+use API\Storage\Provider;
+use API\Storage\Query\StatementResult;
+use API\Controller;
+use API\Config;
+
+use API\Extensions\ExtensionException as Exception;
+
+/**
+ * Mongo Adaptor for this extension
+ */
+class ExtendedStatement extends Provider implements ExtendedStatementInterface
+{
+ /**
+ * Query statements collection
+ * @param array $parameters hashmap of GET params
+ * @return \API\Storage\Query\StatementInterface collection of statement documents
+ */
+ public function extendedQuery($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $collection = 'statements';
+
+ $queryOptions = [];
+
+ // New StatementResult for non-single statement queries
+ $statementResult = new StatementResult();
+
+ // Blank expression
+ $expression = $storage->createExpression();
+
+ // Merge in query
+ if (isset($parameters->query)) {
+ $query = (array)$parameters->query;
+ $expression->fromArray($query);
+ }
+
+ // Add projection
+ if (isset($parameters->projection)) {
+ $fields = (array)$parameters->projection;
+
+ $fields = ['_id' => 1] + $fields;
+ $queryOptions['projection'] = $fields;
+ } else {
+ $queryOptions['projection'] = ['_id' => 1, 'statement' => 1];
+ }
+
+ // Count before paginating
+ $count = $storage->count($collection, $expression, $queryOptions);
+ $statementResult->setTotalCount($count);
+
+ // Handle pagination
+ if (isset($parameters->since_id)) {
+ $id = new \MongoDB\BSON\ObjectID($parameters->since_id);
+ $expression->whereGreater('_id', $id);
+ }
+
+ if (isset($parameters->until_id)) {
+ $id = new \MongoDB\BSON\ObjectID($parameters->until_id);
+ $expression->whereLess('_id', $id);
+ }
+
+ if (isset($parameters->ascending) && $parameters->ascending === 'true') {
+ $statementResult->setSortDescending(false);
+ $statementResult->setSortAscending(true);
+ $queryOptions['sort'] = ['_id' => 1];
+ } else {
+ $statementResult->setSortDescending(true);
+ $statementResult->setSortAscending(false);
+ $queryOptions['sort'] = ['_id' => -1];
+ }
+
+ if (isset($parameters->limit) && $parameters->limit < Config::get(['xAPI', 'statement_get_limit']) && $parameters->limit > 0) {
+ $limit = $parameters->limit;
+ } else {
+ $limit = Config::get(['xAPI', 'statement_get_limit']);
+ }
+
+ // Remaining includes the current page!
+ $statementResult->setRemainingCount($storage->count($collection, $expression, $queryOptions));
+
+ if ($statementResult->getRemainingCount() > $limit) {
+ $statementResult->setHasMore(true);
+ } else {
+ $statementResult->setHasMore(false);
+ }
+
+ $queryOptions['limit'] = (int)$limit;
+
+ $cursor = $storage->find($collection, $expression, $queryOptions);
+ $statementResult->setCursor($cursor);
+
+ return $statementResult;
+ }
+}
diff --git a/src/xAPI/Extensions/ExtendedQuery/Storage/Query/ExtendedStatementInterface.php b/src/xAPI/Extensions/ExtendedQuery/Storage/Query/ExtendedStatementInterface.php
new file mode 100644
index 00000000..59f1f5b1
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/Storage/Query/ExtendedStatementInterface.php
@@ -0,0 +1,35 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery\Storage\Query;
+
+interface ExtendedStatementInterface
+{
+ /**
+ * Query the statements collection
+ * @param array $parameters hashmap of GET params
+ * @return \API\Storage\Query\StatementInterface $statementResult
+ */
+ public function extendedQuery($parameters);
+}
diff --git a/src/xAPI/Extensions/ExtendedQuery/View/V10/ProjectedStatement.php b/src/xAPI/Extensions/ExtendedQuery/View/V10/ProjectedStatement.php
new file mode 100644
index 00000000..1f3f1047
--- /dev/null
+++ b/src/xAPI/Extensions/ExtendedQuery/View/V10/ProjectedStatement.php
@@ -0,0 +1,72 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions\ExtendedQuery\View\V10;
+
+use API\View;
+
+/**
+ * Statement view
+ * @see \API\View
+ */
+class ProjectedStatement extends View
+{
+ /**
+ * Render response view
+ * @param \API\Storage\Query\StatementInterface $statementResult
+ * @return array hashmap of view properites, ready to be serialized into json
+ */
+ public function render($statementResult)
+ {
+ $view = [];
+ $idArray = [];
+ $resultArray = [];
+
+ $view['statements'] = [];
+ $view['more'] = '';
+ $view['totalCount'] = $statementResult->getTotalCount();
+
+ foreach ($statementResult->getCursor() as $result) {
+ if (isset($result->statement)) {
+ $idArray[] = $result->_id;
+ $result = $result->statement;
+ $resultArray[] = $result;
+ }
+ }
+
+ if ($statementResult->getHasMore()) {
+ $latestId = end($idArray);
+ if ($statementResult->getSortDescending()) {
+ $this->getContainer()->get('url')->getQuery()->modify(['until_id' => $latestId]);
+ } else { //Ascending
+ $this->getContainer()->get('url')->getQuery()->modify(['since_id' => $latestId]);
+ }
+ $view['more'] = $this->getContainer()->get('url')->getRelativeUrl();
+ }
+
+ $view['statements'] = array_values($resultArray);
+
+ return $view;
+ }
+}
diff --git a/src/xAPI/Document/Auth/PersistentSession.php b/src/xAPI/Extensions/ExtensionException.php
similarity index 83%
rename from src/xAPI/Document/Auth/PersistentSession.php
rename to src/xAPI/Extensions/ExtensionException.php
index 734f39a4..0841ec64 100644
--- a/src/xAPI/Document/Auth/PersistentSession.php
+++ b/src/xAPI/Extensions/ExtensionException.php
@@ -1,9 +1,8 @@
.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Extensions;
+
+interface ExtensionInterface
+{
+ /**
+ * Returns any event listeners that need to be added for this extension.
+ *
+ * @return array Format: [['event' => 'statement.get', 'callable' => function(), 'priority' => 1 (optional)], [], ...]
+ */
+ public function getEventListeners();
+
+ /**
+ * Returns any routes that need to be added for this extension.
+ *
+ * @return array Format: [['pattern' => '/plus/superstatements', 'callable' => function(), 'methods' => ['GET', 'HEAD']], [], ...]
+ */
+ public function getRoutes();
+
+ /**
+ * Install extension, apply models and configurations
+ *
+ * @return void
+ * @throws \API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function install();
+
+ /**
+ * Provide information for /about endpoint
+ * @see https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#aboutresource
+ * @return array
+ */
+ public function about();
+}
diff --git a/src/xAPI/Document/Auth/OAuthClient.php b/src/xAPI/HttpException.php
similarity index 50%
rename from src/xAPI/Document/Auth/OAuthClient.php
rename to src/xAPI/HttpException.php
index 3e4345d3..453f570f 100644
--- a/src/xAPI/Document/Auth/OAuthClient.php
+++ b/src/xAPI/HttpException.php
@@ -1,9 +1,8 @@
null,
- 'secret' => null,
- 'description' => null,
- 'name' => null,
- 'redirectUri' => null,
- ];
-
- public function relations()
+ private $data = null;
+
+ /**
+ * Prepares a json response exception.
+ *
+ * @see API/Controller::error()
+ *
+ * @param string $message
+ * @param int $statusCode valid httpd status code
+ * @param array|object|null $data extra data to be included in json response
+ * @param \Exception $previous
+ *
+ * @throws \Exception
+ */
+ public function __construct($message, $statusCode = 400, $data = [], \Exception $previous = null)
{
- return [
- 'oAuthTokens' => [self::RELATION_HAS_MANY, 'oAuthTokens', 'clientId'],
- ];
+ $this->data = $data;
+ parent::__construct($message, $statusCode, $previous);
}
- public function jsonSerialize()
+ /**
+ * Get data.
+ *
+ * @return mixed $data
+ */
+ public function getData()
{
- return $this->_data;
- }
-
- public function renderSummary()
- {
- $return = [
- 'name' => $this->_data['name'],
- 'description' => $this->_data['description']
- ];
-
- return $return;
+ return $this->data;
}
}
diff --git a/src/xAPI/Parser/ParserInterface.php b/src/xAPI/Parser/ParserInterface.php
new file mode 100644
index 00000000..c5e52f1e
--- /dev/null
+++ b/src/xAPI/Parser/ParserInterface.php
@@ -0,0 +1,49 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Parser;
+
+interface ParserInterface
+{
+ /**
+ * Get the main part.
+ *
+ * @return ParserResult an object or array, given the payload
+ */
+ public function getData();
+
+ /**
+ * Get the additional parts.
+ *
+ * @return \Traversable an array of the parts
+ */
+ public function getAttachments();
+
+ /**
+ * Get the parts of the request.
+ *
+ * @return \Traversable an array of the parts
+ */
+ public function getParts();
+}
diff --git a/src/xAPI/Parser/ParserResult.php b/src/xAPI/Parser/ParserResult.php
new file mode 100644
index 00000000..2210d505
--- /dev/null
+++ b/src/xAPI/Parser/ParserResult.php
@@ -0,0 +1,134 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Parser;
+
+/**
+ * Storage class for Request parser
+ */
+class ParserResult
+{
+ /**
+ * @var array request params
+ */
+ public $parameters;
+ /**
+ * @var array request headers
+ */
+ public $headers;
+
+ /**
+ * @var string request payload (JSON)
+ */
+ public $rawPayload;
+
+ /**
+ * @var object|array parsed json data
+ */
+ public $payload;
+
+ /**
+ * Get request parameters
+ * @return array
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Set (parsed) request parameters.
+ * @param array $parameters
+ * @return self
+ */
+ public function setParameters($parameters)
+ {
+ $this->parameters = $parameters;
+
+ return $this;
+ }
+
+ /**
+ * Get headers.
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Sets (parsed) headers
+ * @param array $headers the headers
+ * @return self
+ */
+ public function setHeaders($headers)
+ {
+ $this->headers = $headers;
+
+ return $this;
+ }
+
+ /**
+ * Gets rawPayload.
+ * @return string
+ */
+ public function getRawPayload()
+ {
+ return $this->rawPayload;
+ }
+
+ /**
+ * Sets rawPayload.
+ * @param string
+ * @return self
+ */
+ public function setRawPayload($rawPayload)
+ {
+ $this->rawPayload = $rawPayload;
+
+ return $this;
+ }
+
+ /**
+ * Gets (parsed) )payload.
+ * @return array|object
+ */
+ public function getPayload()
+ {
+ return $this->payload;
+ }
+
+ /**
+ * Sets (parsed) payload.
+ * @param array|object $payload (json_decode)
+ * @return self
+ */
+ public function setPayload($payload)
+ {
+ $this->payload = $payload;
+
+ return $this;
+ }
+}
diff --git a/src/xAPI/Parser/PsrRequest.php b/src/xAPI/Parser/PsrRequest.php
new file mode 100644
index 00000000..f46863e9
--- /dev/null
+++ b/src/xAPI/Parser/PsrRequest.php
@@ -0,0 +1,245 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Parser;
+
+use Psr\Http\Message\RequestInterface;
+use API\HttpException;
+use API\Controller;
+
+/**
+ * HTTP request parser
+ */
+class PsrRequest
+{
+ protected $parameters;
+
+ protected $parts;
+
+ protected $payload;
+
+ /**
+ * constructor
+ * @param RequestInterface $request
+ * @return void
+ */
+ public function __construct(RequestInterface $request)
+ {
+ $this->parseRequest($request);
+ }
+
+ /**
+ * Parse http request
+ * @param RequestInterface $request
+ * @return void
+ */
+ public function parseRequest($request)
+ {
+ if ($this->isMultipart($request)) {
+ $this->parts = $this->parseMultipartRequest($request);
+ } else {
+ $this->parts = [$this->parseSingleRequest($request)];
+ }
+ }
+
+ /**
+ * Checks if a request is a multipart request
+ * @param RequestInterface $request
+ * @return boolean
+ */
+ private function isMultipart($request)
+ {
+ return (strpos($request->getMediaType(), 'multipart/') === 0);
+ }
+
+ /**
+ * Strips and parses multipart request body
+ * @param RequestInterface $request
+ * @return array of parsed request body parts
+ */
+ private function parseMultipartRequest($request)
+ {
+ if (false === stripos($request->getContentType(), ';')) {
+ throw new HttpException('Content-Type does not contain a \';\'', Controller::STATUS_BAD_REQUEST);
+ }
+
+ if (!isset($request->getMediaTypeParams()['boundary'])) {
+ throw new HttpException('No boundary present on multipart request.', Controller::STATUS_BAD_REQUEST);
+ }
+ $boundary = $request->getMediaTypeParams()['boundary'];
+
+ // Split bodies by the boundary
+ $bodies = explode('--' . $boundary, (string)$request->getBody());
+
+ // RFC says, to ignore preamble and epilogue.
+ $preamble = array_shift($bodies);
+ $epilogue = array_pop($bodies);
+ $requestParts = [];
+ foreach ($bodies as $body) {
+ $isHeader = true;
+ $headers = [];
+ $content = [];
+ $data = explode(PHP_EOL, $body);
+ foreach ($data as $i => $line) {
+ if (0 == $i) {
+ // Skip the first line
+ array_shift($data);
+ continue;
+ }
+ if ('' == trim($line)) {
+ // Header-body separator
+ $isHeader = false;
+ array_shift($data);
+ continue;
+ }
+ if ($isHeader) {
+ list($header, $value) = explode(':', $line);
+ if ($header) {
+ $headers[strtolower($header)] = explode(',', trim($value));
+ }
+ array_shift($data);
+ } else {
+ $content = implode(PHP_EOL, $data);
+ break;
+ }
+ }
+
+ if (!isset($headers['content-type'])) {
+ $headers['content-type'] = ['text/plain'];
+ }
+
+ $parserResult = new ParserResult();
+
+ $parameters = $request->getQueryParams();
+ $parserResult->setParameters($parameters);
+
+ $parsedHeaders = $this->parseRequestHeaders($request);
+ $parserResult->setHeaders($headers + $parsedHeaders);
+
+ $parserResult->setRawPayload($content);
+
+ if (strpos($headers['content-type'][0], 'application/json') === 0) {
+ $content = json_decode($content);
+
+ // Some clients escape the JSON twice - handle them
+ if (is_string($content)) {
+ $content = json_decode($content);
+ }
+ }
+ $parserResult->setPayload($content);
+
+ // Create request from mock
+ $requestParts[] = $parserResult;
+ }
+
+ if (empty($requestParts)) {
+ throw new HttpException('Invalid multipart request!', Controller::STATUS_BAD_REQUEST);
+ }
+
+ return $requestParts;
+ }
+
+ /**
+ * Get main part of request
+ * @return ParserResult an object or array, given the payload
+ */
+ public function getData()
+ {
+ return $this->parts[0];
+ }
+
+ /**
+ * Get additional parts (attachments) of request.
+ * @return array of ParserResult
+ */
+ public function getAttachments()
+ {
+ // TODO 0.11.x: Test if array_slice works faster than this!
+ $parts = $this->parts;
+ array_shift($parts);
+
+ return $parts;
+ }
+
+ /**
+ * Get all parts of the request.
+ * @return array of ParserResult
+ */
+ public function getParts()
+ {
+ return $this->parts;
+ }
+
+
+ /**
+ * Parses request body
+ * @param RequestInterface $request
+ * @return array|object of parsed request body
+ */
+ private function parseSingleRequest($request)
+ {
+ $parserResult = new ParserResult();
+ $parameters = $request->getQueryParams();
+ // CORS override!
+ if (isset($parameters['method'])) {
+ mb_parse_str($request->getUri()->getQuery(), $parameters);
+ }
+ $parserResult->setParameters($parameters);
+
+ $headers = $request->getHeaders();
+
+ $parsedHeaders = $this->parseRequestHeaders($request);
+ $parserResult->setHeaders($parsedHeaders);
+
+ $body = $request->getBody();
+ $parserResult->setRawPayload($body);
+
+ $parsedBody = $request->getParsedBody();
+ $parserResult->setPayload($parsedBody);
+
+ return $parserResult;
+ }
+
+ /**
+ * Parses and transforms request Headers
+ * @param RequestInterface $request
+ *
+ * @return array selection of parsed request header
+ */
+ private function parseRequestHeaders($request)
+ {
+ $requestHeaders = $request->getHeaders();
+ $parsedHeaders = [];
+
+ // TODO 0.11.x cumbersome logic, improve this, do we really need to strip http- prefix?
+ foreach ($requestHeaders as $key => $value) {
+ $key = strtr(strtolower($key), '_', '-');
+ if (strpos($key, 'http-') === 0) {
+ $key = substr($key, 5);
+ }
+ $parsedHeaders[$key] = $value;
+ }
+ return $parsedHeaders;
+ }
+}
diff --git a/src/xAPI/Resource.php b/src/xAPI/Resource.php
deleted file mode 100644
index 2d5e4f45..00000000
--- a/src/xAPI/Resource.php
+++ /dev/null
@@ -1,237 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API;
-
-use Slim\Slim;
-use Rhumsaa\Uuid\Uuid;
-
-abstract class Resource
-{
- const STATUS_OK = 200;
- const STATUS_CREATED = 201;
- const STATUS_ACCEPTED = 202;
- const STATUS_NO_CONTENT = 204;
-
- const STATUS_MULTIPLE_CHOICES = 300;
- const STATUS_MOVED_PERMANENTLY = 301;
- const STATUS_FOUND = 302;
- const STATUS_NOT_MODIFIED = 304;
- const STATUS_USE_PROXY = 305;
- const STATUS_TEMPORARY_REDIRECT = 307;
-
- const STATUS_BAD_REQUEST = 400;
- const STATUS_UNAUTHORIZED = 401;
- const STATUS_FORBIDDEN = 403;
- const STATUS_NOT_FOUND = 404;
- const STATUS_NOT_FOUND_MESSAGE = 'Cannot find requested resource.';
- const STATUS_METHOD_NOT_ALLOWED = 405;
- const STATUS_METHOD_NOT_ALLOWED_MESSAGE = 'Method %s is not allowed on this resource.';
- const STATUS_NOT_ACCEPTED = 406;
- const STATUS_CONFLICT = 409;
- const STATUS_PRECONDITION_FAILED = 412;
-
- const STATUS_INTERNAL_SERVER_ERROR = 500;
- const STATUS_NOT_IMPLEMENTED = 501;
-
- /**
- * @var \Slim\Slim
- */
- private $slim;
-
- /**
- * Construct.
- */
- public function __construct()
- {
- $this->setSlim(Slim::getInstance());
-
- $this->init();
- }
-
- /**
- * Default init, use for overwrite only.
- */
- public function init()
- {
- }
-
- /**
- * Default get method.
- */
- public function get()
- {
- $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'GET'));
- }
-
- /**
- * Default post method.
- */
- public function post()
- {
- $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'POST'));
- }
-
- /**
- * Default put method.
- */
- public function put()
- {
- $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'PUT'));
- }
-
- /**
- * Default delete method.
- */
- public function delete()
- {
- $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'DELETE'));
- }
-
- /**
- * General options method.
- */
- public function options()
- {
- $this->error(self::STATUS_METHOD_NOT_ALLOWED, sprintf(self::STATUS_METHOD_NOT_ALLOWED_MESSAGE, 'OPTIONS'));
- }
-
- /**
- * Error handler.
- *
- * @param int $code Error code
- * @param string $message Error message
- */
- public static function error($code, $message = '')
- {
- self::jsonResponse($code, ['error_message' => $message]);
- }
-
- /**
- * @param int $status HTTP status code
- * @param array $data The data
- * @param array $allow Allowed methods
- */
- public static function response($status = 200, $data = null, $allow = [])
- {
- /*
- * @var \Slim\Slim
- */
- $slim = \Slim\Slim::getInstance();
-
- $slim->status($status);
- $slim->response->headers->set('Access-Control-Allow-Origin', '*');
- $slim->response->headers->set('Access-Control-Allow-Methods', 'POST,PUT,GET,OPTIONS,DELETE');
- $slim->response->headers->set('Access-Control-Allow-Headers', 'Origin,Content-Type,Authorization,Accept,X-Experience-API-Version,If-Match,If-None-Match');
- $slim->response->headers->set('Access-Control-Allow-Credentials-Control-Allow-Origin', 'true');
- $slim->response->headers->set('Access-Control-Expose-Headers', 'ETag,Last-Modified,Content-Length,X-Experience-API-Version,X-Experience-API-Consistent-Through');
- $slim->response->headers->set('X-Experience-API-Version', $slim->config('xAPI')['latest_version']);
-
- $date = \API\Util\Date::dateTimeToISO8601(\API\Util\Date::dateTimeExact());
- $slim->response->headers->set('X-Experience-API-Consistent-Through', $date);
-
- if (!empty($allow)) {
- $slim->response()->header('Allow', strtoupper(implode(',', $allow)));
- }
-
- $slim->response()->setBody($data);
-
- return false;
- }
-
- public static function jsonResponse($status = 200, $data = [], $allow = [])
- {
- $slim = \Slim\Slim::getInstance();
- $slim->response->headers->set('Content-Type', 'application/json');
- $data = json_encode($data);
- self::response($status, $data, $allow);
- }
-
- public static function multipartResponse($status = 200, $parts = [], $allow = [])
- {
- $slim = \Slim\Slim::getInstance();
- $boundary = Uuid::uuid4()->toString();
- $slim->headers->set('Content-Type', "multipart/mixed; boundary=\"{$boundary}\"");
- $slim->headers->set('Transfer-Encoding', 'chunked');
-
- $content = '';
- foreach ($parts as $part) {
- $content .= "--{$boundary}\r\n";
- $content .= "{$part->headers}\r\n";
- $content .= $part->getContent();
- $content .= "\r\n";
- }
- $content .= "--{$boundary}--";
- // Finally send all the content.
- $content = strlen($content)."\r\n".$content;
-
- self::response($status, $content, $allow);
- }
-
- /**
- * @param $version The xAPI version requested
- * @param $resource The main resource
- * @param $subResource An optional subresource
- *
- * @return mixed
- */
- public static function load($version, $resource, $subResource)
- {
- $versionNamespace = $version->generateClassNamespace();
- if (null !== $subResource) {
- $class = __NAMESPACE__.'\\Resource\\'.$versionNamespace.'\\'.ucfirst($resource).'\\'.ucfirst($subResource);
- } else {
- $class = __NAMESPACE__.'\\Resource\\'.$versionNamespace.'\\'.ucfirst($resource);
- }
- if (!class_exists($class)) {
- return;
- }
-
- return new $class();
- }
-
- /**
- * @return \Slim\Slim
- */
- public function getSlim()
- {
- return $this->slim;
- }
-
- /**
- * @param \Slim\Slim $slim
- */
- public function setSlim($slim)
- {
- $this->slim = $slim;
- }
-
- /**
- * @return \Sokil\Mongo\Client
- */
- public function getDocumentManager()
- {
- return $this->slim->mongo;
- }
-}
diff --git a/src/xAPI/Resource/V10/Activities/Profile.php b/src/xAPI/Resource/V10/Activities/Profile.php
deleted file mode 100644
index 6c1eea13..00000000
--- a/src/xAPI/Resource/V10/Activities/Profile.php
+++ /dev/null
@@ -1,158 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10\Activities;
-
-use API\Resource;
-use API\Service\ActivityProfile as ActivityProfileService;
-use API\View\V10\ActivityProfile as ActivityProfileView;
-
-class Profile extends Resource
-{
- /**
- * @var \API\Service\ActivityProfile
- */
- private $activityProfileService;
-
- /**
- * Get activity service.
- */
- public function init()
- {
- $this->setActivityProfileService(new ActivityProfileService($this->getSlim()));
- }
-
- /**
- * Handle the Statement GET request.
- */
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validateGetRequest($request);
-
- $this->activityProfileService->activityProfileGet($request);
-
- // Render them
- $view = new ActivityProfileView(['service' => $this->activityProfileService]);
-
- if ($this->activityProfileService->getSingle()) {
- $view = $view->renderGetSingle();
- Resource::response(Resource::STATUS_OK, $view);
- } else {
- $view = $view->renderGet();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
- }
-
- public function put()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityProfileService->activityProfilePut($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityProfileService->activityProfilePost($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function delete()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityProfileService->activityProfileDelete($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of activityProfileService.
- *
- * @return \API\Service\ActivityProfile
- */
- public function getActivityProfileService()
- {
- return $this->activityProfileService;
- }
-
- /**
- * Sets the value of activityProfileService.
- *
- * @param \API\Service\ActivityProfile $activityProfileService the activity service
- *
- * @return self
- */
- public function setActivityProfileService(\API\Service\ActivityProfile $activityProfileService)
- {
- $this->activityProfileService = $activityProfileService;
-
- return $this;
- }
-}
diff --git a/src/xAPI/Resource/V10/Activities/State.php b/src/xAPI/Resource/V10/Activities/State.php
deleted file mode 100644
index 38d9736e..00000000
--- a/src/xAPI/Resource/V10/Activities/State.php
+++ /dev/null
@@ -1,158 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10\Activities;
-
-use API\Resource;
-use API\Service\ActivityState as ActivityStateService;
-use API\View\V10\ActivityState as ActivityStateView;
-
-class State extends Resource
-{
- /**
- * @var \API\Service\ActivityState
- */
- private $activityStateService;
-
- /**
- * Get activity service.
- */
- public function init()
- {
- $this->setActivityStateService(new ActivityStateService($this->getSlim()));
- }
-
- /**
- * Handle the Statement GET request.
- */
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('state');
-
- // Do the validation - TODO!!!!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validateGetRequest($request);
-
- $this->activityStateService->activityStateGet($request);
-
- // Render them
- $view = new ActivityStateView(['service' => $this->activityStateService]);
-
- if ($this->activityStateService->getSingle()) {
- $view = $view->renderGetSingle();
- Resource::response(Resource::STATUS_OK, $view);
- } else {
- $view = $view->renderGet();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
- }
-
- public function put()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('state');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityStateService->activityStatePut($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('state');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityStateService->activityStatePost($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function delete()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('state');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->activityStateService->activityStateDelete($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of activityStateService.
- *
- * @return \API\Service\ActivityState
- */
- public function getActivityStateService()
- {
- return $this->activityStateService;
- }
-
- /**
- * Sets the value of activityStateService.
- *
- * @param \API\Service\ActivityState $activityStateService the activity service
- *
- * @return self
- */
- public function setActivityStateService(\API\Service\ActivityState $activityStateService)
- {
- $this->activityStateService = $activityStateService;
-
- return $this;
- }
-}
diff --git a/src/xAPI/Resource/V10/Agents/Profile.php b/src/xAPI/Resource/V10/Agents/Profile.php
deleted file mode 100644
index b5d1712d..00000000
--- a/src/xAPI/Resource/V10/Agents/Profile.php
+++ /dev/null
@@ -1,158 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10\Agents;
-
-use API\Resource;
-use API\Service\AgentProfile as AgentProfileService;
-use API\View\V10\AgentProfile as AgentProfileView;
-
-class Profile extends Resource
-{
- /**
- * @var \API\Service\AgentProfile
- */
- private $agentProfileService;
-
- /**
- * Get agent profile service.
- */
- public function init()
- {
- $this->setagentProfileService(new AgentProfileService($this->getSlim()));
- }
-
- /**
- * Handle the Statement GET request.
- */
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validateGetRequest($request);
-
- $this->agentProfileService->agentProfileGet($request);
-
- // Render them
- $view = new AgentProfileView(['service' => $this->agentProfileService]);
-
- if ($this->agentProfileService->getSingle()) {
- $view = $view->renderGetSingle();
- Resource::response(Resource::STATUS_OK, $view);
- } else {
- $view = $view->renderGet();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
- }
-
- public function put()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->agentProfileService->agentProfilePut($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->agentProfileService->agentProfilePost($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function delete()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('profile');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->agentProfileService->agentProfileDelete($request);
-
- //Always an empty response, unless there was an Exception
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of agentProfileService.
- *
- * @return \API\Service\AgentProfile
- */
- public function getAgentProfileService()
- {
- return $this->agentProfileService;
- }
-
- /**
- * Sets the value of agentProfileService.
- *
- * @param \API\Service\AgentProfile $agentProfileService the agent service
- *
- * @return self
- */
- public function setAgentProfileService(\API\Service\AgentProfile $agentProfileService)
- {
- $this->agentProfileService = $agentProfileService;
-
- return $this;
- }
-}
diff --git a/src/xAPI/Resource/V10/Auth/Tokens.php b/src/xAPI/Resource/V10/Auth/Tokens.php
deleted file mode 100644
index 5c48de2f..00000000
--- a/src/xAPI/Resource/V10/Auth/Tokens.php
+++ /dev/null
@@ -1,155 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10\Auth;
-
-use API\Resource;
-use API\Service\Auth\Basic as BasicTokenService;
-use API\View\V10\BasicAuth\AccessToken as AccessTokenView;
-
-class Tokens extends Resource
-{
- /**
- * @var \API\Service\AccessToken
- */
- private $accessTokenService;
-
- /**
- * Get agent profile service.
- */
- public function init()
- {
- $this->setAccessTokenService(new BasicTokenService($this->getSlim()));
- }
-
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('super');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- $this->accessTokenService->accessTokenGet($request);
-
- // Render them
- $view = new AccessTokenView(['service' => $this->accessTokenService]);
-
- $view = $view->render();
-
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('super');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- $this->accessTokenService->accessTokenPost($request);
-
- // Render them
- $view = new AccessTokenView(['service' => $this->accessTokenService]);
-
- $view = $view->render();
-
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
-
- public function put()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('super');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- $this->accessTokenService->accessTokenPut($request);
-
- // Render them
- $view = new AccessTokenView(['service' => $this->accessTokenService]);
-
- $view = $view->render();
-
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
-
- public function delete()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('super');
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- $this->accessTokenService->accessTokenDelete($request);
-
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of accessTokenService.
- *
- * @return \API\Service\AccessToken
- */
- public function getAccessTokenService()
- {
- return $this->accessTokenService;
- }
-
- /**
- * Sets the value of accessTokenService.
- *
- * @param \API\Service\AccessToken $accessTokenService the access token service
- *
- * @return self
- */
- public function setAccessTokenService(\API\Service\Auth\Basic $accessTokenService)
- {
- $this->accessTokenService = $accessTokenService;
-
- return $this;
- }
-}
diff --git a/src/xAPI/Resource/V10/Oauth/Authorize.php b/src/xAPI/Resource/V10/Oauth/Authorize.php
deleted file mode 100644
index 99847a59..00000000
--- a/src/xAPI/Resource/V10/Oauth/Authorize.php
+++ /dev/null
@@ -1,153 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10\Oauth;
-
-use API\Resource;
-use API\Service\Auth\OAuth as OAuthService;
-use API\Service\User as UserService;
-use API\View\V10\OAuth\Authorize as OAuthAuthorizeView;
-use API\Util\OAuth;
-
-class Authorize extends Resource
-{
- /**
- * @var \API\Service\Auth\OAuth
- */
- private $oAuthService;
-
- /**
- * @var \API\Service\User
- */
- private $userService;
-
- /**
- * Get agent profile service.
- */
- public function init()
- {
- $this->setOAuthService(new OAuthService($this->getSlim()));
- $this->setUserService(new UserService($this->getSlim()));
- OAuth::loadSession();
- }
-
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- if ($this->userService->loggedIn()) {
- $this->oAuthService->authorizeGet($request);
- // Authorization is always requested
- $view = new OAuthAuthorizeView(['service' => $this->oAuthService, 'userService' => $this->userService]);
- $view = $view->renderGet();
- Resource::response(Resource::STATUS_OK, $view);
- } else {
- // Redirect to login
- $redirectUrl = $this->getSlim()->url;
- $redirectUrl->getPath()->remove('authorize');
- $redirectUrl->getPath()->append('login');
- $this->getSlim()->response->headers->set('Location', $redirectUrl);
- Resource::response(Resource::STATUS_FOUND);
- }
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Do the validation - TODO!!!
- //$this->statementValidator->validateRequest($request);
- //$this->statementValidator->validatePutRequest($request);
-
- if ($this->userService->loggedIn()) {
- // Authorization is always requested
- $this->oAuthService->authorizePost($request);
- $redirectUri = $this->oAuthService->getRedirectUri();
- $this->getSlim()->response->headers->set('Location', $redirectUri);
- Resource::response(Resource::STATUS_FOUND);
- } else {
- // Unauthorized
- Resource::response(Resource::STATUS_UNAUTHORIZED);
- }
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * Gets the value of oAuthService.
- *
- * @return \API\Service\Auth\OAuth
- */
- public function getOAuthService()
- {
- return $this->oAuthService;
- }
-
- /**
- * Sets the value of oAuthService.
- *
- * @param \API\Service\Auth\OAuth $oAuthService the o auth service
- *
- * @return self
- */
- public function setOAuthService(\API\Service\Auth\OAuth $oAuthService)
- {
- $this->oAuthService = $oAuthService;
-
- return $this;
- }
-
- /**
- * Gets the value of userService.
- *
- * @return \API\Service\User
- */
- public function getUserService()
- {
- return $this->userService;
- }
-
- /**
- * Sets the value of userService.
- *
- * @param \API\Service\User $userService the user service
- *
- * @return self
- */
- public function setUserService(\API\Service\User $userService)
- {
- $this->userService = $userService;
-
- return $this;
- }
-}
diff --git a/src/xAPI/Resource/V10/Plus/Test.php b/src/xAPI/Resource/V10/Plus/Test.php
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/xAPI/Resource/V10/Statements.php b/src/xAPI/Resource/V10/Statements.php
deleted file mode 100644
index 5b8662ee..00000000
--- a/src/xAPI/Resource/V10/Statements.php
+++ /dev/null
@@ -1,223 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Resource\V10;
-
-use API\Resource;
-use API\Service\Statement as StatementService;
-use API\Validator\V10\Statement as StatementValidator;
-use API\View\V10\Statements as StatementView;
-
-class Statements extends Resource
-{
- /**
- * @var \API\Service\Statement
- */
- private $statementService;
-
- /**
- * @var \API\Validator\Statement
- */
- private $statementValidator;
-
- /**
- * Get statement service.
- */
- public function init()
- {
- $this->setStatementService(new StatementService($this->getSlim()));
- $this->setStatementValidator(new StatementValidator());
- $this->getStatementValidator()->setDefaultSchemaValidator();
- }
-
- /**
- * Handle the Statement GET request.
- */
- public function get()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission(['statements/read', 'statements/read/mine']);
-
- // Do the validation
- $this->statementValidator->validateRequest($request);
- $this->statementValidator->validateGetRequest($request);
-
- // Load the statements - this needs to change, drastically, as it's garbage
- $this->statementService->statementGet($request);
-
- // Render them
- $view = new StatementView(['service' => $this->statementService]);
-
- if ($this->statementService->getSingle()) {
- $view = $view->renderGetSingle();
- } else {
- $view = $view->renderGet();
- }
-
- // Multipart responses are intentionally disabled for now
- //if (null === $attachments) {
- $this->setHeaders();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- //} else {
- // $this->setHeaders();
- // Resource::multipartResponse(Resource::STATUS_OK, $view, $attachments);
- //}
- }
-
- public function put()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('statements/write');
-
- // Do the validation
- $this->statementValidator->validateRequest($request);
- $this->statementValidator->validatePutRequest($request);
-
- // Save the statements
- $this->statementService->statementPut($request);
-
- //Always an empty response, unless there was an Exception
- $this->setHeaders();
- Resource::response(Resource::STATUS_NO_CONTENT);
- }
-
- public function post()
- {
- $request = $this->getSlim()->request();
-
- // Check authentication
- $this->getSlim()->auth->checkPermission('statements/write');
-
- // Do the validation and multipart splitting
- $this->statementValidator->validateRequest($request);
-
- if ($request->isMultipart()) {
- $jsonRequest = $this->extractJsonRequestFromMultipart($request);
- } else {
- $jsonRequest = $request;
- }
-
- $this->statementValidator->validatePostRequest($jsonRequest);
-
- // Save the statements
- $this->statementService->statementPost($request);
-
- $view = new StatementView(['service' => $this->statementService]);
- $view = $view->renderPost();
-
- $this->setHeaders();
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
-
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'POST,PUT,GET,DELETE');
- Resource::response(Resource::STATUS_OK);
- }
-
- /**
- * @return \API\Service\Statement
- */
- public function getStatementService()
- {
- return $this->statementService;
- }
-
- /**
- * @param \API\Service\Statement $statementService
- */
- public function setStatementService($statementService)
- {
- $this->statementService = $statementService;
- }
-
- /**
- * @return \API\Validator\Statement
- */
- public function getStatementValidator()
- {
- return $this->statementValidator;
- }
-
- /**
- * @param \API\Validator\Statement $statementValidator
- */
- public function setStatementValidator($statementValidator)
- {
- $this->statementValidator = $statementValidator;
- }
-
- /**
- * @return array
- */
- public function getOptions()
- {
- return $this->options;
- }
-
- /**
- * Extracts JSON request from multipart/mixed request.
- *
- * @param \Slim\Http\Request $request Request object
- *
- * @return \Slim\Http\Request Request object
- */
- protected function extractJsonRequestFromMultipart($request)
- {
- $jsonRequest = $request->parts()->get(0);
-
- return $jsonRequest;
- }
-
- /**
- * Extracts all attachment requests from main request.
- *
- * @param \Slim\Http\Request $request Whole request
- *
- * @return array Array of \Slim\Http\Request's that represent attachments
- */
- protected function extractAttachmentsFromRequest($request)
- {
- $requests = $request->parts()->all();
- array_shift($requests);
-
- return $requests;
- }
-
- /**
- * Sets specific headers for this request
- *
- * @return void
- */
-
- protected function setHeaders()
- {
- }
-
-}
diff --git a/src/xAPI/Resource/V11/Activities.php b/src/xAPI/Resource/V11/Activities.php
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/xAPI/Routes.php b/src/xAPI/Routes.php
new file mode 100644
index 00000000..eb395969
--- /dev/null
+++ b/src/xAPI/Routes.php
@@ -0,0 +1,149 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from slim.
+ * License information is available at https://github.com/slimphp/Slim/blob/3.x/LICENSE.md
+ *
+ */
+
+namespace API;
+
+class Routes
+{
+
+ /**
+ * @var array $routes default routes
+ *
+ * pattern:
+ * [
+ * (string) "module": core app module (or "extension"),
+ * (array) "methods": array of HTTP methods for this route
+ * (string) "description": a short description for the LRS /about view
+ * (string) "controller: fully namespaced controller class name,
+ * ]
+ */
+ private static $routes = [
+
+ // xAPI routes
+
+ '/about' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'OPTIONS'],
+ 'description' =>'LRS Information',
+ 'controller' => 'API\\Controller\\V10\\About',
+ ],
+ '/activities' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'OPTIONS'],
+ 'description' =>'Activity Object Storage/Retrieval',
+ 'controller' => 'API\\Controller\\V10\\Activities',
+ ],
+ '/activities/profile' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
+ 'description' =>'Activity Profile Resource',
+ 'controller' => 'API\\Controller\\V10\\Activities\\Profile',
+ ],
+ '/activities/state' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
+ 'description' =>'State Resource',
+ 'controller' => 'API\\Controller\\V10\\Activities\\State',
+ ],
+ '/agents' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'OPTIONS'],
+ 'description' =>'Agent Object Storage/Retrieval',
+ 'controller' => 'API\\Controller\\V10\\Agents',
+ ],
+ '/agents/profile' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
+ 'description' =>'Agent Profile Resource',
+ 'controller' => 'API\\Controller\\V10\\Agents\\Profile',
+ ],
+ '/statements' => [
+ 'module' => 'xAPI',
+ 'methods' => ['GET', 'PUT', 'POST', 'OPTIONS'],
+ 'description' =>'Statement Storage/Retrieval',
+ 'controller' => 'API\\Controller\\V10\\Statements',
+ ],
+
+ // oAuth routes
+
+ '/oauth/authorize' => [
+ 'module' => 'oAuth',
+ 'methods' => ['GET', 'POST', 'OPTIONS'],
+ 'description' =>'Resource Owner Authorization',
+ 'controller' => 'API\\Controller\\V10\\Oauth\\Authorize',
+ ],
+ '/oauth/login' => [
+ 'module' => 'oAuth',
+ 'methods' => ['GET', 'POST', 'OPTIONS'],
+ 'description' =>'Resource Owner Login',
+ 'controller' => 'API\\Controller\\V10\\Oauth\\Login',
+ ],
+ '/oauth/token' => [
+ 'module' => 'oAuth',
+ 'methods' => ['POST', 'OPTIONS'],
+ 'description' =>'Token Request',
+ 'controller' => 'API\\Controller\\V10\\Oauth\\Token',
+ ],
+
+ // custom
+
+ '/attachments' => [
+ 'module' => 'Storage',
+ 'methods' => ['GET', 'OPTIONS'],
+ 'description' =>'Attachment file retrieval',
+ 'controller' => 'API\\Controller\\V10\\Attachments',
+ ],
+
+ '/auth/tokens' => [
+ 'module' => 'Auth',
+ 'methods' => ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
+ 'description' =>'Temporary BASIC token endpoint',
+ 'controller' => 'API\\Controller\\V10\\Auth\\Tokens',
+ ],
+ ];
+
+ /**
+ * Returns all routes
+ *
+ * @return array self::$routes
+ */
+ public function all()
+ {
+ return self::$routes;
+ }
+
+ /**
+ * Merges an array of new routes into self::$routes.
+ * The merging order ensures that extising routes are not overwritten by new routes
+ *
+ * @return void
+ */
+ public function merge(array $routes)
+ {
+ self::$routes = array_merge($routes, self::$routes);
+ }
+}
diff --git a/src/xAPI/Service.php b/src/xAPI/Service.php
index d97a2b10..f2f5ca05 100644
--- a/src/xAPI/Service.php
+++ b/src/xAPI/Service.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,41 +26,13 @@
abstract class Service
{
- /**
- * @var \Slim\Slim
- */
- private $slim;
-
- /**
- * Constructor.
- *
- * @param \Slim\Slim $slim Slim framework
- */
- public function __construct($slim)
- {
- $this->setSlim($slim);
- }
-
- /**
- * @return \Sokil\Mongo\Client
- */
- public function getDocumentManager()
- {
- return $this->getSlim()->mongo;
- }
+ use BaseTrait;
/**
- * @return \Slim\Slim
- */
- public function getSlim()
- {
- return $this->slim;
- }
- /**
- * @param \Slim\Slim $slim
+ * @constructor
*/
- public function setSlim($slim)
+ public function __construct($container)
{
- $this->slim = $slim;
+ $this->setContainer($container);
}
}
diff --git a/src/xAPI/Service/Activity.php b/src/xAPI/Service/Activity.php
index eec2e63e..ae93d073 100644
--- a/src/xAPI/Service/Activity.php
+++ b/src/xAPI/Service/Activity.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,33 +25,10 @@
namespace API\Service;
use API\Service;
-use API\Resource;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
+use API\Util\Collection;
class Activity extends Service
{
- /**
- * Activities.
- *
- * @var array
- */
- protected $activities;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single activity state fetch?
- *
- * @var bool
- */
- protected $single = false;
-
/**
* Fetches activity profiles according to the given parameters.
*
@@ -59,94 +36,13 @@ class Activity extends Service
*
* @return array An array of activityProfile objects.
*/
- public function activityGet($request)
+ public function activityGet()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activities');
- $cursor = $collection->find();
-
- $cursor->where('id', $params->get('activityId'));
-
- if ($cursor->count() === 0) {
- throw new Exception('Activity does not exist.', Resource::STATUS_NOT_FOUND);
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
-
- /**
- * Gets the Activities.
- *
- * @return array
- */
- public function getActivities()
- {
- return $this->activityProfiles;
- }
-
- /**
- * Sets the Activities.
- *
- * @param array $activities the activities
- *
- * @return self
- */
- public function setActivities(array $activities)
- {
- $this->activities = $activities;
-
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single activity fetch?.
- *
- * @return bool
- */
- public function getSingle()
- {
- return $this->single;
- }
-
- /**
- * Sets the Is this a single activity fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
- {
- $this->single = $single;
+ $activityDocument = $this->getStorage()->getActivityStorage()->fetchById($params->get('activityId'));
- return $this;
+ return $activityDocument;
}
}
diff --git a/src/xAPI/Service/ActivityProfile.php b/src/xAPI/Service/ActivityProfile.php
index 46e63c7a..643e9499 100644
--- a/src/xAPI/Service/ActivityProfile.php
+++ b/src/xAPI/Service/ActivityProfile.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,35 +25,10 @@
namespace API\Service;
use API\Service;
-use API\Resource;
-use API\Util;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
-use DateTime;
+use API\Util\Collection;
class ActivityProfile extends Service
{
- /**
- * Activity profiles.
- *
- * @var array
- */
- protected $activityProfiles;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single activity state fetch?
- *
- * @var bool
- */
- protected $single = false;
-
/**
* Fetches activity profiles according to the given parameters.
*
@@ -61,130 +36,32 @@ class ActivityProfile extends Service
*
* @return array An array of activityProfile objects.
*/
- public function activityProfileGet($request)
+ public function activityProfileGet()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityProfiles');
- $cursor = $collection->find();
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- // Single activity state
- if ($params->has('profileId')) {
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('activityId', $params->get('activityId'));
+ $documentResult = $this->getStorage()->getActivityProfileStorage()->getFiltered($params);
- if ($cursor->count() === 0) {
- throw new Exception('Activity state does not exist.', Resource::STATUS_NOT_FOUND);
- }
-
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
-
- $cursor->where('activityId', $params->get('activityId'));
-
- if ($params->has('since')) {
- $date = Util\Date::dateRFC3339($params->get('since'));
- if(!$date){
- throw new Exception('"since" parameter is not a valid ISO 8601 timestamp.(Good example: 2015-11-18T12:17:00+00:00), ', Resource::STATUS_NOT_FOUND);
- }
- $since = Util\Date::dateTimeToMongoDate($date);
- $cursor->whereGreaterOrEqual('mongoTimestamp', $since);
- }
-
- $this->cursor = $cursor;
-
- return $this;
+ return $documentResult;
}
/**
* Tries to save (merge) an activityProfile.
*/
- public function activityProfilePost($request)
+ public function activityProfilePost()
{
- $params = new Set($request->get());
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
// Validation has been completed already - everything is assumed to be valid
- $rawBody = $request->getBody();
-
- $collection = $this->getDocumentManager()->getCollection('activityProfiles');
-
- // Set up the body to be saved
- $activityProfileDocument = $collection->createDocument();
-
- // Check for existing state - then merge if applicable
- $cursor = $collection->find();
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('activityId', $params->get('activityId'));
-
- $result = $cursor->findOne();
-
- // Check If-Match and If-None-Match here - these SHOULD* exist, but they do not have to
- // See https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI.md#lrs-requirements-7
- // if (!$request->headers('If-Match') && !$request->headers('If-None-Match') && $result) {
- // throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- // }
-
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
+ $rawBody = $request->getRawPayload();
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
+ $params->set('headers', $request->getHeaders());
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
+ $documentResult = $this->getStorage()->getActivityProfileStorage()->post($params, $rawBody);
- // ID exists, try to merge body if applicable
- if ($result) {
- if ($result->getContentType() !== 'application/json') {
- throw new \Exception('Original document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- if ($contentType !== 'application/json') {
- throw new \Exception('Posted document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- $decodedExisting = json_decode($result->getContent(), true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON in existing document. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
-
- $decodedPosted = json_decode($rawBody, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON posted. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
-
- $rawBody = json_encode(array_merge($decodedExisting, $decodedPosted));
- $activityProfileDocument = $result;
- }
-
- $activityProfileDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $activityProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $activityProfileDocument->setActivityId($params->get('activityId'));
- $activityProfileDocument->setProfileId($params->get('profileId'));
- $activityProfileDocument->setContentType($contentType);
- $activityProfileDocument->setHash(sha1($rawBody));
- $activityProfileDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('activityProfiles', $activityProfileDocument)->save();
-
- $this->single = true;
- $this->activityStates = [$activityProfileDocument];
-
- return $this;
+ return $documentResult;
}
/**
@@ -192,71 +69,19 @@ public function activityProfilePost($request)
*
* @return
*/
- public function activityProfilePut($request)
+ public function activityProfilePut()
{
- // Validation has been completed already - everyhing is assumed to be valid (from an external view!)
- $rawBody = $request->getBody();
-
- // Single
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityProfiles');
-
- $activityProfileDocument = $collection->createDocument();
-
- // Check for existing state - then replace if applicable
- $cursor = $collection->find();
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('activityId', $params->get('activityId'));
-
- $result = $cursor->findOne();
-
- // Check If-Match and If-None-Match here
- if (!$request->headers('If-Match') && !$request->headers('If-Match') && $result) {
- throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
-
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
-
- // ID exists, replace body
- if ($result) {
- $activityProfileDocument = $result;
- }
-
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
-
- $activityProfileDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $activityProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $activityProfileDocument->setActivityId($params->get('activityId'));
- $activityProfileDocument->setProfileId($params->get('profileId'));
- $activityProfileDocument->setContentType($contentType);
- $activityProfileDocument->setHash(sha1($rawBody));
- $activityProfileDocument->save();
+ // Validation has been completed already - everything is assumed to be valid
+ $rawBody = $request->getRawPayload();
- // Add to log
- $this->getSlim()->requestLog->addRelation('activityProfiles', $activityProfileDocument)->save();
+ $params->set('headers', $request->getHeaders());
- $this->single = true;
- $this->activityProfiles = [$activityProfileDocument];
+ $documentResult = $this->getStorage()->getActivityProfileStorage()->put($params, $rawBody);
- return $this;
+ return $documentResult;
}
/**
@@ -266,131 +91,15 @@ public function activityProfilePut($request)
*
* @return self Nothing.
*/
- public function activityProfileDelete($request)
- {
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityProfiles');
- $cursor = $collection->find();
-
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('activityId', $params->get('activityId'));
-
- $result = $cursor->findOne();
-
- if (!$result) {
- throw new \Exception('Profile does not exist!.', Resource::STATUS_NOT_FOUND);
- }
-
- // Check If-Match and If-None-Match here - these SHOULD* exist, but they do not have to
- // See https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI.md#lrs-requirements-7
- // if (!$request->headers('If-Match') && !$request->headers('If-None-Match') && $result) {
- // throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- // }
-
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
-
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('activityProfiles', $result)->save();
-
- $result->delete();
-
- return $this;
- }
-
- /**
- * Trims quotes from the header.
- *
- * @param string $headerString Header
- *
- * @return string Trimmed header
- */
- private function trimHeader($headerString)
+ public function activityProfileDelete()
{
- return trim($headerString, '"');
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- /**
- * Gets the Activity states.
- *
- * @return array
- */
- public function getActivityProfiles()
- {
- return $this->activityProfiles;
- }
-
- /**
- * Sets the Activity profiles.
- *
- * @param array $activityProfiles the activity profiles
- *
- * @return self
- */
- public function setActivityProfiles(array $activityProfiles)
- {
- $this->activityProfiles = $activityProfiles;
+ $params->set('headers', $request->getHeaders());
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single activity state fetch?.
- *
- * @return bool
- */
- public function getSingle()
- {
- return $this->single;
- }
-
- /**
- * Sets the Is this a single activity state fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
- {
- $this->single = $single;
+ $deletionResult = $this->getStorage()->getActivityProfileStorage()->delete($params);
- return $this;
+ return $deletionResult;
}
}
diff --git a/src/xAPI/Service/ActivityState.php b/src/xAPI/Service/ActivityState.php
index 8702798a..a0f2e501 100644
--- a/src/xAPI/Service/ActivityState.php
+++ b/src/xAPI/Service/ActivityState.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,209 +25,41 @@
namespace API\Service;
use API\Service;
-use API\Resource;
-use API\Util;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
+use API\Util\Collection;
class ActivityState extends Service
{
- /**
- * Activity states.
- *
- * @var array
- */
- protected $activityStates;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single activity state fetch?
- *
- * @var bool
- */
- protected $single = false;
-
/**
* Fetches activity states according to the given parameters.
*
- * @param array $request The incoming HTTP request
- *
* @return array An array of statement objects.
*/
- public function activityStateGet($request)
+ public function activityStateGet()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityStates');
- $cursor = $collection->find();
-
- // Single activity state
- if ($params->has('stateId')) {
- $cursor->where('stateId', $params->get('stateId'));
- $cursor->where('activityId', $params->get('activityId'));
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- } else {
- throw new Exception('Invalid request!', Resource::STATUS_BAD_REQUEST);
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($params->has('registration')) {
- $cursor->where('registration', $params->get('registration'));
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- if ($cursor->count() === 0) {
- throw new Exception('Activity state does not exist.', Resource::STATUS_NOT_FOUND);
- }
+ $documentResult = $this->getStorage()->getActivityStateStorage()->getFiltered($params);
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
-
- $cursor->where('activityId', $params->get('activityId'));
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- } else {
- throw new Exception('Invalid request!', Resource::STATUS_BAD_REQUEST);
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($params->has('registration')) {
- $cursor->where('registration', $params->get('registration'));
- }
-
- if ($params->has('since')) {
- $date = Util\Date::dateRFC3339($params->get('since'));
- if(!$date){
- throw new Exception('"since" parameter is not a valid ISO 8601 timestamp.(Good example: 2015-11-18T12:17:00+00:00), ', Resource::STATUS_NOT_FOUND);
- }
- $since = Util\Date::dateTimeToMongoDate($date);
- $cursor->whereGreaterOrEqual('mongoTimestamp', $since);
- }
-
- $this->cursor = $cursor;
-
- return $this;
+ return $documentResult;
}
/**
* Tries to save (merge) an activityState.
*/
- public function activityStatePost($request)
+ public function activityStatePost()
{
- $params = new Set($request->get());
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
// Validation has been completed already - everything is assumed to be valid
- $rawBody = $request->getBody();
-
- $collection = $this->getDocumentManager()->getCollection('activityStates');
-
- // Set up the body to be saved
- $activityStateDocument = $collection->createDocument();
-
- // Check for existing state - then merge if applicable
- $cursor = $collection->find();
- $cursor->where('stateId', $params->get('stateId'));
- $cursor->where('activityId', $params->get('activityId'));
-
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- } else {
- throw new Exception('Invalid request!', Resource::STATUS_BAD_REQUEST);
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($params->has('registration')) {
- $cursor->where('registration', $params->get('registration'));
- }
-
- $result = $cursor->findOne();
-
- // ID exists, merge body
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
-
- // ID exists, try to merge body if applicable
- if ($result) {
- if ($result->getContentType() !== 'application/json') {
- throw new \Exception('Original document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- if ($contentType !== 'application/json') {
- throw new \Exception('Posted document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- $decodedExisting = json_decode($result->getContent(), true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON in existing document. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
-
- $decodedPosted = json_decode($rawBody, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON posted. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
+ $rawBody = $request->getRawPayload();
- $rawBody = json_encode(array_merge($decodedExisting, $decodedPosted));
- $activityStateDocument = $result;
- }
+ $params->set('headers', $request->getHeaders());
- $activityStateDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $activityStateDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
+ $documentResult = $this->getStorage()->getActivityStateStorage()->post($params, $rawBody);
- $activityStateDocument->setActivityId($params->get('activityId'));
- $activityStateDocument->setAgent($agent);
- if ($params->has('registration')) {
- $activityStateDocument->setRegistration($params->get('registration'));
- }
- $activityStateDocument->setStateId($params->get('stateId'));
- $activityStateDocument->setContentType($contentType);
- $activityStateDocument->setHash(sha1($rawBody));
- $activityStateDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('activityStates', $activityStateDocument)->save();
-
- $this->single = true;
- $this->activityStates = [$activityStateDocument];
-
- return $this;
+ return $documentResult;
}
/**
@@ -235,121 +67,34 @@ public function activityStatePost($request)
*
* @return
*/
- public function activityStatePut($request)
+ public function activityStatePut()
{
- // Validation has been completed already - everyhing is assumed to be valid (from an external view!)
- $rawBody = $request->getBody();
-
- // Single
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityStates');
-
- $activityStateDocument = $collection->createDocument();
-
- // Check for existing state - then replace if applicable
- $cursor = $collection->find();
- $cursor->where('stateId', $params->get('stateId'));
- $cursor->where('activityId', $params->get('activityId'));
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- } else {
- throw new Exception('Invalid request!', Resource::STATUS_BAD_REQUEST);
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($params->has('registration')) {
- $cursor->where('registration', $params->get('registration'));
- }
-
- $result = $cursor->findOne();
-
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
-
- // ID exists, replace
- if ($result) {
- $activityStateDocument = $result;
- }
-
- $activityStateDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $activityStateDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $activityStateDocument->setActivityId($params->get('activityId'));
-
- $activityStateDocument->setAgent($agent);
- if ($params->has('registration')) {
- $activityStateDocument->setRegistration($params->get('registration'));
- }
- $activityStateDocument->setStateId($params->get('stateId'));
- $activityStateDocument->setContentType($contentType);
- $activityStateDocument->setHash(sha1($rawBody));
- $activityStateDocument->save();
+ // Validation has been completed already - everything is assumed to be valid
+ $rawBody = $request->getRawPayload();
- // Add to log
- $this->getSlim()->requestLog->addRelation('activityStates', $activityStateDocument)->save();
+ $params->set('headers', $request->getHeaders());
- $this->single = true;
- $this->activityStates = [$activityStateDocument];
+ $documentResult = $this->getStorage()->getActivityStateStorage()->put($params, $rawBody);
- return $this;
+ return $documentResult;
}
/**
* Fetches activity states according to the given parameters.
*
- * @param array $request The incoming HTTP request
- *
* @return array An array of statement objects.
*/
- public function activityStateDelete($request)
+ public function activityStateDelete()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('activityStates');
-
- $expression = $collection->expression();
-
- if ($params->has('stateId')) {
- $expression->where('stateId', $params->get('stateId'));
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $expression->where('activityId', $params->get('activityId'));
+ $params->set('headers', $request->getHeaders());
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- } else {
- throw new Exception('Invalid request!', Resource::STATUS_BAD_REQUEST);
- }
- $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($params->has('registration')) {
- $expression->where('registration', $params->get('registration'));
- }
-
- $collection->deleteDocuments($expression);
+ $this->getStorage()->getActivityStateStorage()->delete($params);
return $this;
}
@@ -388,20 +133,6 @@ public function getCursor()
return $this->cursor;
}
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
/**
* Gets the Is this a single activity state fetch?.
*
diff --git a/src/xAPI/Service/AgentProfile.php b/src/xAPI/Service/AgentProfile.php
index 9bc47f01..28281b50 100644
--- a/src/xAPI/Service/AgentProfile.php
+++ b/src/xAPI/Service/AgentProfile.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,193 +25,41 @@
namespace API\Service;
use API\Service;
-use API\Resource;
-use API\Util;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
+use API\Util\Collection;
class AgentProfile extends Service
{
- /**
- * Activity profiles.
- *
- * @var array
- */
- protected $agentProfiles;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single activity state fetch?
- *
- * @var bool
- */
- protected $single = false;
-
/**
* Fetches agent profiles according to the given parameters.
*
- * @param array $request The incoming HTTP request
- *
* @return array An array of agentProfile objects.
*/
- public function agentProfileGet($request)
+ public function agentProfileGet()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('agentProfiles');
- $cursor = $collection->find();
-
- // Single activity profile
- if ($params->has('profileId')) {
- $cursor->where('profileId', $params->get('profileId'));
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
-
- if ($cursor->count() === 0) {
- throw new Exception('Agent profile does not exist.', Resource::STATUS_NOT_FOUND);
- }
-
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $agent = $params->get('agent');
- $agent = json_decode($agent);
- $cursor->where('agent', $agent);
+ $documentResult = $this->getStorage()->getAgentProfileStorage()->getFiltered($params);
- if ($params->has('since')) {
- $date = Util\Date::dateRFC3339($params->get('since'));
- if(!$date){
- throw new Exception('"since" parameter is not a valid ISO 8601 timestamp.(Good example: 2015-11-18T12:17:00+00:00), ', Resource::STATUS_NOT_FOUND);
- }
- $since = Util\Date::dateTimeToMongoDate($date);
- $cursor->whereGreaterOrEqual('mongoTimestamp', $since);
- }
-
- $this->cursor = $cursor;
-
- return $this;
+ return $documentResult;
}
/**
* Tries to save (merge) an agentProfile.
*/
- public function agentProfilePost($request)
+ public function agentProfilePost()
{
- $params = new Set($request->get());
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
// Validation has been completed already - everything is assumed to be valid
- $rawBody = $request->getBody();
-
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- }
-
- $collection = $this->getDocumentManager()->getCollection('agentProfiles');
-
- // Set up the body to be saved
- $agentProfileDocument = $collection->createDocument();
-
- // Check for existing state - then merge if applicable
- $cursor = $collection->find();
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+ $rawBody = $request->getRawPayload();
- $result = $cursor->findOne();
+ $params->set('headers', $request->getHeaders());
- // Check If-Match and If-None-Match here - these SHOULD* exist, but they do not have to
- // See https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI.md#lrs-requirements-7
- // if (!$request->headers('If-Match') && !$request->headers('If-None-Match') && $result) {
- // throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- // }
+ $documentResult = $this->getStorage()->getAgentProfileStorage()->post($params, $rawBody);
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
-
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
-
- // ID exists, merge body
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
-
- // ID exists, try to merge body if applicable
- if ($result) {
- if ($result->getContentType() !== 'application/json') {
- throw new \Exception('Original document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- if ($contentType !== 'application/json') {
- throw new \Exception('Posted document is not JSON. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
- $decodedExisting = json_decode($result->getContent(), true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON in existing document. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
-
- $decodedPosted = json_decode($rawBody, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON posted. Cannot merge!', Resource::STATUS_BAD_REQUEST);
- }
-
- $rawBody = json_encode(array_merge($decodedExisting, $decodedPosted));
- $agentProfileDocument = $result;
- }
-
- $agentProfileDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $agentProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $agentProfileDocument->setAgent($agent);
- $agentProfileDocument->setProfileId($params->get('profileId'));
- $agentProfileDocument->setContentType($contentType);
- $agentProfileDocument->setHash(sha1($rawBody));
- $agentProfileDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('agentProfiles', $agentProfileDocument)->save();
-
- $this->single = true;
- $this->activityStates = [$agentProfileDocument];
-
- return $this;
+ return $documentResult;
}
/**
@@ -219,154 +67,35 @@ public function agentProfilePost($request)
*
* @return
*/
- public function agentProfilePut($request)
+ public function agentProfilePut()
{
- // Validation has been completed already - everyhing is assumed to be valid (from an external view!)
- $rawBody = $request->getBody();
- $body = json_decode($rawBody, true);
-
- // Some clients escape the JSON - handle them
- if (is_string($body)) {
- $body = json_decode($body, true);
- }
-
- // Single
- $params = new Set($request->get());
-
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- }
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $collection = $this->getDocumentManager()->getCollection('agentProfiles');
+ $params->set('headers', $request->getHeaders());
- $agentProfileDocument = $collection->createDocument();
+ $rawBody = $request->getRawPayload();
- // Check for existing state - then replace if applicable
- $cursor = $collection->find();
- $cursor->where('profileId', $params->get('profileId'));
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+ $documentResult = $this->getStorage()->getAgentProfileStorage()->put($params, $rawBody);
- $result = $cursor->findOne();
-
- // Check If-Match and If-None-Match here
- if (!$request->headers('If-Match') && !$request->headers('If-Match') && $result) {
- throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- }
-
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
-
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
-
- // ID exists, replace body
- if ($result) {
- $agentProfileDocument = $result;
- }
-
- $contentType = $request->headers('Content-Type');
- if ($contentType === null) {
- $contentType = 'text/plain';
- }
-
- $agentProfileDocument->setContent($rawBody);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $agentProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
-
- $agentProfileDocument->setAgent($agent);
- $agentProfileDocument->setProfileId($params->get('profileId'));
- $agentProfileDocument->setContentType($contentType);
- $agentProfileDocument->setHash(sha1($rawBody));
- $agentProfileDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('agentProfiles', $agentProfileDocument)->save();
-
- $this->single = true;
- $this->activityProfiles = [$agentProfileDocument];
-
- return $this;
+ return $documentResult;
}
/**
* Fetches activity states according to the given parameters.
*
- * @param array $request The incoming HTTP request
- *
* @return self Nothing.
*/
- public function agentProfileDelete($request)
+ public function agentProfileDelete()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('agentProfiles');
- $cursor = $collection->find();
+ $request = $this->getContainer()->get('parser')->getData();
+ $params = new Collection($request->getParameters());
- $cursor->where('profileId', $params->get('profileId'));
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- }
- $cursor->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+ $params->set('headers', $request->getHeaders());
- $result = $cursor->findOne();
+ $deletionResult = $this->getStorage()->getAgentProfileStorage()->delete($params);
- if (!$result) {
- throw new \Exception('Profile does not exist!.', Resource::STATUS_NOT_FOUND);
- }
-
- // Check If-Match and If-None-Match here - these SHOULD* exist, but they do not have to
- // See https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI.md#lrs-requirements-7
- // if (!$request->headers('If-Match') && !$request->headers('If-None-Match') && $result) {
- // throw new \Exception('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Resource::STATUS_CONFLICT);
- // }
-
- // If-Match first
- if ($request->headers('If-Match') && $result && ($this->trimHeader($request->headers('If-Match')) !== $result->getHash())) {
- throw new \Exception('If-Match header doesn\'t match the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
-
- // Then If-None-Match
- if ($request->headers('If-None-Match')) {
- if ($this->trimHeader($request->headers('If-None-Match')) === '*' && $result) {
- throw new \Exception('If-None-Match header is *, but a resource already exists.', Resource::STATUS_PRECONDITION_FAILED);
- } elseif ($result && $this->trimHeader($request->headers('If-None-Match')) === $result->getHash()) {
- throw new \Exception('If-None-Match header matches the current ETag.', Resource::STATUS_PRECONDITION_FAILED);
- }
- }
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('agentProfiles', $result)->save();
-
- $result->delete();
-
- return $this;
+ return $deletionResult;
}
/**
@@ -380,76 +109,4 @@ private function trimHeader($headerString)
{
return trim($headerString, '"');
}
-
- /**
- * Gets the Agent profiles.
- *
- * @return array
- */
- public function getAgentProfiles()
- {
- return $this->agentProfiles;
- }
-
- /**
- * Sets the Agent profiles.
- *
- * @param array $agentProfiles the agent profiles
- *
- * @return self
- */
- public function setAgentProfiles(array $agentProfiles)
- {
- $this->agentProfiles = $agentProfiles;
-
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single agent profile fetch?.
- *
- * @return bool
- */
- public function getSingle()
- {
- return $this->single;
- }
-
- /**
- * Sets the Is this a single agent profile fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
- {
- $this->single = $single;
-
- return $this;
- }
}
diff --git a/src/xAPI/Service/Attachment.php b/src/xAPI/Service/Attachment.php
index 6bbb4cab..c15bda27 100644
--- a/src/xAPI/Service/Attachment.php
+++ b/src/xAPI/Service/Attachment.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,6 +25,7 @@
namespace API\Service;
use API\Service;
+use API\Config;
class Attachment extends Service
{
@@ -37,12 +38,7 @@ class Attachment extends Service
*/
public function fetchMetadataBySha2($sha2)
{
- $collection = $this->getDocumentManager()->getCollection('attachments');
- $cursor = $collection->find();
-
- $cursor->where('sha2', $sha2);
-
- $document = $cursor->current();
+ $document = $this->getStorage()->getAttachmentStorage()->fetchMetadataBySha2($sha2);
return $document;
}
@@ -56,7 +52,7 @@ public function fetchMetadataBySha2($sha2)
*/
public function fetchFileBySha2($sha2)
{
- $fsAdapter = \API\Util\Filesystem::generateAdapter($this->getSlim()->config('filesystem'));
+ $fsAdapter = \API\Util\Filesystem::generateAdapter(Config::get('filesystem'));
$contents = $fsAdapter->read($sha2);
return $contents;
diff --git a/src/xAPI/Service/Auth.php b/src/xAPI/Service/Auth.php
new file mode 100644
index 00000000..41027c90
--- /dev/null
+++ b/src/xAPI/Service/Auth.php
@@ -0,0 +1,247 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Service;
+
+use API\Service;
+use API\Config;
+use API\Bootstrap;
+
+use API\HttpException;
+
+/**
+ * Inheritance writes the inherited permissions into the token and user records AS IT IS ON TIME OF CREATION,
+ * Applying changed permissions (config) requires to re-issue the token
+ *
+ * Flat permissions: Permission inheritances of the config are merged into a flat, unique array of permissions inside the token or user document.
+ * Creation time only: a user / token permissions are created on date of creation and don't change when the config was changed
+ * One level inheritance only: Inheritance is a first-level only merge. It doesn\'t include childs of childs (only first children)
+ * Unknown permissions in user/token stored documents are ignored
+ *
+ * We do not considder inheritance changes on run-time as this can be troublesome.
+ * Such a dynamic permission assignment is a ROLE behaviour and planned for a future release.
+ * Roles are understood as goups of permissions who can be updated at any time an reflect the changes.
+*/
+class Auth extends Service
+{
+ /**
+ * @var string $userId user Mongo ObjectId
+ */
+ private $userId = null;
+
+ /**
+ * @var array $permissions current token/auth permissions, not to be confused with global user permissions
+ */
+ private $permissions = [];
+
+ /**
+ * @var array $scopes Available permission scopes (cached)
+ */
+ private $scopes;
+
+ /**
+ * @constructor
+ */
+ public function __construct($container, $authScopes = null)
+ {
+ parent::__construct($container);
+ $this->scopes = Config::get(['xAPI', 'supported_auth_scopes'], []);
+ }
+
+
+ /**
+ * Mock AuthScopes for unit testing
+ *
+ * @return string|Null Mongo ObjectId
+ */
+ public function mockAuthScopes(array $scopes)
+ {
+ if(Bootstrap::mode() !== Bootstrap::Testing) {
+ throw new \RunTimeException('Mocking AuthScopes is not allowed in this Bootstrap Mode');
+ }
+ $this->scopes = $scopes;
+ }
+
+ /**
+ * Register a user auth, user Id and permissions
+ * @param string|\MongoDB\BSON\ObjectID $userId user record _id
+ * @param array $scopeIds token names as string
+ *
+ * @return void
+ */
+ public function register($userId, array $permissionNames)
+ {
+ $this->userId = (string) $userId;
+
+ // map storerd token permissions against current configuration
+ // filter out token permissions who are not part of the current configuration
+ $filtered = $this->filterPermissions($permissionNames);
+
+ // we do not update inheritance here
+ $this->permissions = $filtered;
+ // $this->permissions = $this->mergeInheritance($filtered);
+ }
+
+ /**
+ * Gets the current UserId
+ *
+ * @return string|Null Mongo ObjectId
+ */
+ public function getUserId()
+ {
+ return $this->userId;
+ }
+
+ /**
+ * Gets the current permissions
+ *
+ * @return array permission names
+ */
+ public function getPermissions()
+ {
+ return $this->permissions;
+ }
+
+ /**
+ * Merges inherited permissions into an array of given permission names
+ * Invalid permission names (not in config) are ignored.
+ * @see self::$scopes
+ * @param array $permissions array of permission names
+ *
+ * @return string|Null Mongo ObjectId
+ */
+ public function mergeInheritance(array $permissions)
+ {
+ $merged = $permissions;
+ foreach($permissions as $name) {
+ $merged = array_merge($merged, $this->getInheritanceFor($name));
+ }
+ return array_unique($merged);
+ }
+
+ /**
+ * Get inherited permissions for a sinfgle permissiion
+ * An invalid permission name (not in config) will be ignored.
+ * @see self::$scopes
+ * @param string $name permission name
+ *
+ * @return array inherited permissions (not including $name!)
+ */
+ public function getInheritanceFor(string $name)
+ {
+ if (!isset($this->scopes[$name])) {
+ return [];
+ }
+ if(empty($this->scopes[$name]['inherits'])){
+ return [];
+ }
+ // yaml parses empty arrays to {}
+ return (array) $this->scopes[$name]['inherits'];
+ }
+
+ /**
+ * Gets the registered AuthScopes
+ *
+ * @return array
+ */
+ public function getAuthScopes()
+ {
+ return $this->scopes;
+ }
+
+ /**
+ * Gets single AuthScope by permission name
+ *
+ * @return array|false
+ */
+ public function getAuthScope(string $name)
+ {
+ return (isset($this->scopes[$name])) ? $this->scopes[$name] : false;
+ }
+
+ /**
+ * Checks if a permission is set for the current user auth
+ *
+ * @return bool
+ */
+ public function hasPermission(string $name)
+ {
+ return in_array($name, $this->permissions);
+ }
+
+ /**
+ * Checks if a permission is set for the user auth and throws Exception
+ * if queried permission is not assigned to the user auth
+ *
+ * @return bool
+ */
+ public function requirePermission(string $name)
+ {
+ if (!in_array($name, $this->permissions)){
+ throw new HttpException('Unauthorized', 401);
+ }
+
+ // this was mapped in constructor already however in this case it's better to check twice
+ if (!isset($this->scopes[$name])){
+ throw new HttpException('Unauthorized', 401);
+ }
+
+ }
+
+ /**
+ * Public alias for self::filterPermissions
+ * @param array $permissionNames
+ *
+ * @return array registered and valid permissions
+ *
+ */
+ public function sanitizePermissions(array $permissionNames)
+ {
+ return $this->filterPermissions($permissionNames);
+ }
+
+ /**
+ * Filters and sanitizes submitted permission names against configured permission names
+ * @param array $permissionNames
+ *
+ * @return array registered and valid permissions
+ *
+ */
+ private function filterPermissions(array $permissionNames)
+ {
+ $configured = array_keys($this->scopes);
+ return array_filter($permissionNames, function($name) use ($configured) {
+ // TODO 0.10.x Issue warning to logger
+ if(!is_string($name)) {
+ return false;
+ }
+ if(empty($name)) {
+ return false;
+ }
+ return in_array($name, $configured);
+ // TODO 0.10.x Issue warning
+ });
+ }
+
+}
diff --git a/src/xAPI/Service/Auth/AuthInterface.php b/src/xAPI/Service/Auth/AuthInterface.php
index 33893010..59ca2020 100644
--- a/src/xAPI/Service/Auth/AuthInterface.php
+++ b/src/xAPI/Service/Auth/AuthInterface.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/xAPI/Service/Auth/Basic.php b/src/xAPI/Service/Auth/Basic.php
index cf90634d..3b52effa 100644
--- a/src/xAPI/Service/Auth/Basic.php
+++ b/src/xAPI/Service/Auth/Basic.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,69 +25,22 @@
namespace API\Service\Auth;
use API\Service;
-use API\Resource;
-use Slim\Helper\Set;
+use API\Controller;
use Slim\Http\Request;
-use API\Document\User;
use API\Service\User as UserService;
+use API\Service\Auth as AuthService;
use API\Util;
+use API\Util\Collection;
-class Basic extends Service implements AuthInterface
-{
- /**
- * Access tokens.
- *
- * @var array
- */
- protected $accessTokens;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
+use API\HttpException as Exception;
+use API\Service\Auth\Exception as AuthFailureException;
- /**
- * Is this a single access token fetch?
- *
- * @var bool
- */
- protected $single = false;
- public function addToken($name, $description, $expiresAt, User $user, array $scopes = [])
+class Basic extends Service implements AuthInterface
+{
+ public function addToken($name, $description, $expiresAt, $user, array $scopes = [], $key = null, $secret = null)
{
- $collection = $this->getDocumentManager()->getCollection('basicTokens');
-
- $accessTokenDocument = $collection->createDocument();
-
- $accessTokenDocument->setName($name);
- $accessTokenDocument->setDescription($description);
- $accessTokenDocument->addRelation('user', $user);
- $scopeIds = [];
- foreach ($scopes as $scope) {
- $scopeIds[] = $scope->getId();
- }
- $accessTokenDocument->setScopeIds($scopeIds);
-
- if (isset($expiresAt)) {
- $expiresDate = new \DateTime();
- $expiresDate->setTimestamp($expiresAt);
- $accessTokenDocument->setExpiresAt(\API\Util\Date::dateTimeToMongoDate($expiresDate));
- }
-
- //Generate token
- $accessTokenDocument->setKey(\API\Util\OAuth::generateToken());
- $accessTokenDocument->setSecret(\API\Util\OAuth::generateToken());
-
- $currentDate = new \DateTime();
- $accessTokenDocument->setCreatedAt(\API\Util\Date::dateTimeToMongoDate($currentDate));
-
- $accessTokenDocument->save();
-
- $this->single = true;
- $this->setAccessTokens([$accessTokenDocument]);
-
+ $accessTokenDocument = $this->getStorage()->getBasicAuthStorage()->storeToken($name, $description, $expiresAt, $user, $scopes, $key, $secret);
return $accessTokenDocument;
}
@@ -101,26 +54,7 @@ public function addToken($name, $description, $expiresAt, User $user, array $sco
*/
public function fetchToken($key, $secret)
{
- $collection = $this->getDocumentManager()->getCollection('basicTokens');
- $cursor = $collection->find();
-
- $cursor->where('key', $key);
- $cursor->where('secret', $secret);
- $accessTokenDocument = $cursor->current();
-
- if ($accessTokenDocument === null) {
- throw new \Exception('Invalid credentials.', Resource::STATUS_FORBIDDEN);
- }
-
- $expiresAt = $accessTokenDocument->getExpiresAt();
-
- if ($expiresAt !== null) {
- if ($expiresAt->sec <= time()) {
- throw new \Exception('Expired token.', Resource::STATUS_FORBIDDEN);
- }
- }
-
- $this->setAccessTokens([$accessTokenDocument]);
+ $accessTokenDocument = $this->getStorage()->getBasicAuthStorage()->getToken($key, $secret);
return $accessTokenDocument;
}
@@ -132,21 +66,9 @@ public function fetchToken($key, $secret)
*
* @return [type] [description]
*/
- public function deleteToken($key, $secret = null)
+ public function deleteToken($key)
{
- $collection = $this->getDocumentManager()->getCollection('basicTokens');
-
- $expression = $collection->expression();
-
- $expression->where('key', $key);
-
- if (null != $secret) {
- $expression->where('secret', $secret);
- }
-
- $collection->deleteDocuments($expression);
-
- return $this;
+ $this->getStorage()->getBasicAuthStorage()->deleteToken($key);
}
/**
@@ -159,15 +81,7 @@ public function deleteToken($key, $secret = null)
*/
public function expireToken($key)
{
- $collection = $this->getDocumentManager()->getCollection('basicTokens');
- $cursor = $collection->find();
- $cursor->where('key', $key);
-
- $accessTokenDocument = $cursor->current();
- $accessTokenDocument->setExpired(true);
- $accessTokenDocument->save();
-
- $this->setAccessTokens([$accessTokenDocument]);
+ $accessTokenDocument = $this->getStorage()->getBasicAuthStorage()->expireToken($key);
return $accessTokenDocument;
}
@@ -179,26 +93,9 @@ public function expireToken($key)
*/
public function fetchTokens()
{
- $collection = $this->getDocumentManager()->getCollection('basicTokens');
- $cursor = $collection->find();
-
- $this->setCursor($cursor);
-
- return $this;
- }
-
- public function getScopeByName($name)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- $cursor = $collection->find();
- $cursor->where('name', $name);
- $scopeDocument = $cursor->current();
-
- if (null === $scopeDocument) {
- throw new \Exception('Invalid scope given!', Resource::STATUS_BAD_REQUEST);
- }
+ $cursor = $this->getStorage()->getBasicAuthStorage()->getTokens();
- return $scopeDocument;
+ return $cursor;
}
/**
@@ -206,103 +103,80 @@ public function getScopeByName($name)
*/
public function accessTokenGet($request)
{
- $params = new Set($request->get());
+ $params = new Collection($request->get());
+ $token = $this->fetchToken($params->get('key'), $params->get('secret'));
- $this->fetchToken($params->get('key'), $params->get('secret'));
-
- return $this;
+ return $token;
}
/**
* Tries to create a new access token.
*/
- public function accessTokenPost($request)
+ public function accessTokenPost()
{
- $body = $request->getBody();
- $body = json_decode($body, true);
+ $body = $this->getContainer()->get('parser')->getData()->getPayload();
+ $this->validateRequiredParams($body);
+ $currentDate = new \DateTime();
- // Some clients escape the JSON - handle them
- if (is_string($body)) {
- $body = json_decode($body, true);
- }
+ $parsedParams = (object)[
+ 'user' => (object)[
+ 'email' => $body->user->email,
+ 'name' => isset($body->user->name) ? $body->user->name : 'anonymous',
+ 'description' => isset($body->user->description) ? $body->user->description : '',
+ 'password' => isset($body->user->password) ? $body->user->password : 'password',
+ 'permissions' => isset($body->user->permissions) ? $body->user->permissions : ['all'],
+ ],
+ 'scopes' => isset($body->scopes) ? $body->scopes : ['all'],
+ 'name' => 'Token for '.$body->user->email,
+ 'description' => 'Token generated at '.Util\Date::dateTimeToISO8601($currentDate),
+ 'expiresAt' => isset($body->expiresAt) ? $body->expiresAt : null,
+ ];
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('Invalid JSON posted. Cannot continue!', Resource::STATUS_BAD_REQUEST);
- }
+ $permissionService = new AuthService($this->getContainer());
- $requestParams = new Set($body);
+ // Sanitize submitted token permissions
+ $scopes = $parsedParams->scopes;
+ $scopeDocuments = $permissionService->sanitizePermissions($scopes);
- if ($requestParams->get('user')['email'] === null) {
- throw new \Exception('Invalid request, user.email property not present!', Resource::STATUS_BAD_REQUEST);
- }
+ // Sanitize submitted user permissions
+ $permissions = $parsedParams->user->permissions;
+ $permissionDocuments = $permissionService->sanitizePermissions($permissions);
- $currentDate = new \DateTime();
+ // You cannot create a user with more permissions than the user associated with the API call
+ $callingTokenPermissions = $this->getContainer()->get('auth')->getPermissions();
- $defaultParams = new Set([
- 'user' => [
- 'password' => 'password',
- 'permissions' => [
- 'all'
- ]
- ],
- 'scopes' => [
- 'all'
- ],
- 'name' => 'Token for ' . $requestParams->get('user')['email'],
- 'description' => 'Token generated at ' . Util\Date::dateTimeToISO8601($currentDate),
- 'expiresAt' => null
- ]);
-
- $params = new Set(array_replace_recursive($defaultParams->all(), $requestParams->all()));
-
- $scopeDocuments = [];
- $scopes = $params->get('scopes');
- foreach ($scopes as $scope) {
- $scopeDocument = $this->getScopeByName($scope);
- $scopeDocuments[] = $scopeDocument;
+ // User-permisisons can only be a subset of callingTokenPermissions
+ if (!(array_intersect($parsedParams->user->permissions, $callingTokenPermissions) == $parsedParams->user->permissions)) {
+ // $parsedParams->scopes is not a subset of $parsedParams->user->permissions
+ throw new Exception('Permissions array cannot contain more permissions that the used accessToken associated users\' permissions!', Controller::STATUS_BAD_REQUEST);
}
- $permissionDocuments = [];
- $permissions = $params->get('user')['permissions'];
- foreach ($permissions as $permission) {
- $permissionDocument = $this->getScopeByName($permission);
- $permissionDocuments[] = $permissionDocument;
+ // Scopes can only be a subset of user->permissions
+ if (!(array_intersect($parsedParams->scopes, $parsedParams->user->permissions) == $parsedParams->scopes)) {
+ // $parsedParams->scopes is not a subset of $parsedParams->user->permissions
+ throw new Exception('Scopes array cannot contain more permissions that the associated users\' permissions!', Controller::STATUS_BAD_REQUEST);
}
- if (is_numeric($params->get('expiresAt'))) {
- $expiresAt = $params->get('expiresAt');
- } else if (null === $params->get('expiresAt')) {
- $expiresAt = null;
- } else {
- $expiresAt = new \DateTime($params->get('expiresAt'));
- $expiresAt = $expiresAt->getTimestamp();
+ // Temporary super tokens cannot be created
+ if (in_array('super', $parsedParams->scopes) || in_array('super', $parsedParams->scopes)) {
+ throw new Exception('Tokens and users with super permissions cannot be created using this endpoint!', Controller::STATUS_BAD_REQUEST);
}
- $userService = new UserService($this->getSlim());
- $email = $params->get('user')['email'];
- $password = $params->get('user')['password'];
- // Account exists
- if ($userService->getEmailCount($email) > 0) {
- $correctUserFound = false;
- $usersCursor = $userService->findByEmail($email);
- foreach ($usersCursor as $matchedUser) {
- if ($matchedUser->getPassword() === sha1($password)) {
- $correctUserFound = true;
- $user = $matchedUser;
- break;
- }
- }
- if (!$correctUserFound) {
- throw new \Exception('Invalid credentials.', Resource::STATUS_UNAUTHORIZED);
- }
+ if (is_numeric($parsedParams->expiresAt)) {
+ $expiresAt = $parsedParams->expiresAt;
+ } elseif (null === $parsedParams->expiresAt) {
+ $expiresAt = null;
} else {
- $user = $userService->addUser($email, $password, $permissionDocuments);
- $user->save();
+ $expiresAt = new \DateTime($parsedParams->expiresAt);
+ $expiresAt = $expiresAt->getTimestamp();
}
- $this->addToken($params->get('name'), $params->get('description'), $expiresAt, $user, $scopeDocuments);
+ // TODO 0.11.x: This functionality (user creation + token creation should be in two separate API calls)
+ $userService = new UserService($this->getContainer());
+ $user = $userService->addUser($parsedParams->user->name, $parsedParams->user->description, $parsedParams->user->email, $parsedParams->user->password, $permissionDocuments)->toArray();
+ $accessTokenDocument = $this->addToken($parsedParams->name, $parsedParams->description, $expiresAt, $user, $scopeDocuments);
- return $this;
+ return $accessTokenDocument;
}
/**
@@ -310,8 +184,7 @@ public function accessTokenPost($request)
*/
public function accessTokenDelete($request)
{
- $params = new Set($request->get());
-
+ $params = new Collection($request->get());
$this->deleteToken($params->get('key'), $params->get('secret'));
return $this;
@@ -319,20 +192,15 @@ public function accessTokenDelete($request)
public function extractToken(Request $request)
{
- $headers = $request->headers();
- $rawHeaders = $request->rawHeaders();
- if (isset($rawHeaders['Authorization'])) {
- $header = $rawHeaders['Authorization'];
- } elseif (isset($headers['Authorization'])) {
- $header = $headers['Authorization'];
- } else {
- throw new Exception('Authorization header required.');
+ $header = $request->getHeaderLine('Authorization');
+ if (!$header) {
+ throw new AuthFailureException('Authorization header required.');
}
if (preg_match('/Basic\s+(.*)$/i', $header, $matches)) {
$str = base64_decode($matches[1]);
} else {
- throw new Exception('Authorization header invalid.');
+ throw new AuthFailureException('Authorization header invalid.');
}
$components = explode(':', $str);
@@ -342,81 +210,23 @@ public function extractToken(Request $request)
try {
$token = $this->fetchToken($authUser, $authPass);
} catch (\Exception $e) {
- throw new Exception('Authorization header invalid.');
+ throw new AuthFailureException('Authorization header invalid.');
}
return $token;
}
- /**
- * Gets the Access tokens.
- *
- * @return array
- */
- public function getAccessTokens()
- {
- return $this->accessTokens;
- }
-
- /**
- * Sets the Access tokens.
- *
- * @param array $accessTokens the access tokens
- *
- * @return self
- */
- public function setAccessTokens(array $accessTokens)
- {
- $this->accessTokens = $accessTokens;
-
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor($cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single access token fetch?.
- *
- * @return bool
- */
- public function getSingle()
+ private function validateJsonDecodeErrors()
{
- return $this->single;
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new Exception('Invalid JSON in existing document. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the Is this a single access token fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
+ private function validateRequiredParams($requestParams)
{
- $this->single = $single;
-
- return $this;
+ if (!isset($requestParams->user->email) || $requestParams->user->email === null) {
+ throw new Exception('Invalid request, user.email property not present!', Controller::STATUS_BAD_REQUEST);
+ }
}
}
diff --git a/src/xAPI/Service/Auth/Exception.php b/src/xAPI/Service/Auth/Exception.php
index 09e1fc84..615a51cd 100644
--- a/src/xAPI/Service/Auth/Exception.php
+++ b/src/xAPI/Service/Auth/Exception.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/xAPI/Service/Auth/OAuth.php b/src/xAPI/Service/Auth/OAuth.php
index 8c592695..a885a038 100644
--- a/src/xAPI/Service/Auth/OAuth.php
+++ b/src/xAPI/Service/Auth/OAuth.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,262 +25,94 @@
namespace API\Service\Auth;
use API\Service;
-use API\Resource;
-use Slim\Helper\Set;
-use API\Document\User;
-use API\Document\Auth\OAuthClient;
+use API\Controller;
use Slim\Http\Request;
use API\Util;
use League\Url\Url;
+use API\HttpException as Exception;
+use API\Service\Auth\Exception as AuthFailureException;
+use API\Util\Collection;
+use API\Config;
class OAuth extends Service implements AuthInterface
{
- /**
- * Access tokens.
- *
- * @var array
- */
- protected $accessTokens;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single access token fetch?
- *
- * @var bool
- */
- protected $single = false;
-
- /**
- * The relevant client(s).
- *
- * @var \API\Document\Auth\OAuthClient
- */
- protected $client;
-
- /**
- * The relevant scopes.
- *
- * @var array
- */
- protected $scopes;
-
- /**
- * The relevant token.
- *
- * @var \API\Document\Auth\OAuthToken
- */
- protected $token;
-
- /**
- * The relevant redirectUri.
- *
- * @var string
- */
- protected $redirectUri;
-
- public function addToken($expiresAt, User $user, OAuthClient $client, array $scopes = [], $code = null)
+ public function addToken($expiresAt, $user, $client, array $scopes = [], $code = null)
{
- $collection = $this->getDocumentManager()->getCollection('oAuthTokens');
-
- $accessTokenDocument = $collection->createDocument();
-
- $expiresDate = new \DateTime();
- $expiresDate->setTimestamp($expiresAt);
- $accessTokenDocument->setExpiresAt(\API\Util\Date::dateTimeToMongoDate($expiresDate));
- $currentDate = new \DateTime();
- $accessTokenDocument->setCreatedAt(\API\Util\Date::dateTimeToMongoDate($currentDate));
- $accessTokenDocument->addRelation('user', $user);
- $accessTokenDocument->addRelation('client', $client);
- $scopeIds = [];
- foreach ($scopes as $scope) {
- $scopeIds[] = $scope->getId();
- }
- $accessTokenDocument->setScopeIds($scopeIds);
-
- $accessTokenDocument->setToken(Util\OAuth::generateToken());
- if (null !== $code) {
- $accessTokenDocument->setCode($code);
- }
-
- $accessTokenDocument->save();
- $this->single = true;
- $this->setAccessTokens([$accessTokenDocument]);
+ $accessTokenDocument = $this->getStorage()->getOAuthStorage()->storeToken($expiresAt, $user, $client, $scopes, $code);
return $accessTokenDocument;
}
public function fetchToken($accessToken)
{
- $collection = $this->getDocumentManager()->getCollection('oAuthTokens');
- $cursor = $collection->find();
-
- $cursor->where('token', $accessToken);
- $accessTokenDocument = $cursor->current();
-
- if ($accessTokenDocument === null) {
- throw new \Exception('Invalid access token specified.', Resource::STATUS_FORBIDDEN);
- }
-
- $expiresAt = $accessTokenDocument->getExpiresAt();
-
- if ($expiresAt !== null) {
- if ($expiresAt->sec <= time()) {
- throw new \Exception('Expired token.', Resource::STATUS_FORBIDDEN);
- }
- }
-
- $this->setAccessTokens([$accessTokenDocument]);
+ $accessTokenDocument = $this->getStorage()->getOAuthStorage()->getToken($accessToken);
return $accessTokenDocument;
}
public function deleteToken($accessToken)
{
- $collection = $this->getDocumentManager()->getCollection('oAuthTokens');
-
- $expression = $collection->expression();
- $expression->where('token', $accessToken);
- $collection->deleteDocuments($expression);
+ $accessTokenDocument = $this->getStorage()->getOAuthStorage()->deleteToken($accessToken);
- return $this;
+ return $accessTokenDocument;
}
public function expireToken($accessToken)
{
- $collection = $this->getDocumentManager()->getCollection('oAuthTokens');
- $cursor = $collection->find();
-
- $cursor->where('token', $accessToken);
- $accessTokenDocument = $cursor->current();
- $accessTokenDocument->setExpired(true);
- $accessTokenDocument->save();
-
- $this->setAccessTokens([$accessTokenDocument]);
+ $accessTokenDocument = $this->getStorage()->getOAuthStorage()->expireToken($accessToken);
- return $document;
+ return $accessTokenDocument;
}
public function addClient($name, $description, $redirectUri)
{
- $collection = $this->getDocumentManager()->getCollection('oAuthClients');
-
- // Set up the Client to be saved
- $clientDocument = $collection->createDocument();
-
- $clientDocument->setName($name);
-
- $clientDocument->setDescription($description);
-
- $clientDocument->setRedirectUri($redirectUri);
-
- $clientId = Util\OAuth::generateToken();
- $clientDocument->setClientId($clientId);
-
- $secret = Util\OAuth::generateToken();
- $clientDocument->setSecret($secret);
-
- $clientDocument->save();
-
- $this->single = true;
- $this->client = [$clientDocument];
+ $clientDocument = $this->getStorage()->getOAuthClientsStorage()->addClient($name, $description, $redirectUri);
return $clientDocument;
}
public function fetchClients()
{
- $collection = $this->getDocumentManager()->getCollection('oAuthClients');
- $cursor = $collection->find();
-
- $this->setCursor($cursor);
+ $documentResult = $this->getStorage()->getOAuthClientsStorage()->getClients();
- return $this;
- }
-
- public function addScope($name, $description)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
-
- // #104, check if scope document record exists already
- $exists = $collection->find()->where('name', $name)->findOne();
- if ($exists) {
- return false;
- }
-
- // Set up the Client to be saved
- $scopeDocument = $collection->createDocument();
- $scopeDocument->setName($name);
- $scopeDocument->setDescription($description);
- $scopeDocument->save();
-
- $this->single = true;
- $this->scopes = [$scopeDocument];
-
- return $scopeDocument;
+ return $documentResult;
}
+ // TODO 0.12.x: Move this logic a level higher, into Controllers (low-priority)
/**
* @param [type] $request [description]
*
* @return [type] [description]
*/
- public function authorizeGet($request)
+ public function authorizeGet()
{
// CSRF protection
$_SESSION['csrfToken'] = Util\OAuth::generateCsrfToken();
- $params = new Set($request->get());
+ $parameters = (object)$this->getContainer()->get('parser')->getData()->getParameters();
$requiredParams = ['response_type', 'client_id', 'redirect_uri', 'scope'];
+ $this->validateRequiredParams($parameters, $requiredParams);
- //TODO: Use json-schema validator
- foreach ($requiredParams as $requiredParam) {
- if (!$params->has($requiredParam)) {
- throw new \Exception('Parameter '.$requiredParam.' is missing!', Resource::STATUS_BAD_REQUEST);
- }
- }
-
- if ($params->get('response_type') !== 'code') {
- throw new \Exception('Invalid response_type specified.', Resource::STATUS_BAD_REQUEST);
- }
-
- $collection = $this->getDocumentManager()->getCollection('oAuthClients');
- $cursor = $collection->find();
+ $this->validateResponseType($parameters->response_type);
- $cursor->where('clientId', $params->get('client_id'));
- $clientDocument = $cursor->current();
+ // Get client by id
+ $clientDocument = $this->getStorage()->getOAuthClientsStorage()->getClientById($parameters->client_id);
- if (null === $clientDocument) {
- throw new \Exception('Invalid client_id', Resource::STATUS_BAD_REQUEST);
- }
+ $this->validateClientDocument($clientDocument);
- if ($params->get('redirect_uri') !== $clientDocument->getRedirectUri()) {
- throw new \Exception('Redirect_uri mismatch!', Resource::STATUS_BAD_REQUEST);
- }
+ $this->validateRedirectUri($parameters->redirect_uri, $clientDocument);
- $collection = $this->getDocumentManager()->getCollection('authScopes');
$scopeDocuments = [];
- $scopes = explode(',', $params->get('scope'));
- foreach ($scopes as $scope) {
- $cursor = $collection->find();
- $cursor->where('name', $scope);
- $scopeDocument = $cursor->current();
- if (null === $scopeDocument) {
- throw new \Exception('Invalid scope given!', Resource::STATUS_BAD_REQUEST);
- }
- $scopeDocuments[] = $scopeDocument;
- }
+ $scopes = explode(',', $parameters->scope);
+ $scopeDocuments = $this->validateAndMapScopes($scopes); // Throws exception if not a valid scope
- $this->client = $clientDocument;
- $this->scopes = $scopeDocuments;
+ // Return client document object with added authorize request scopes
+ $clientDocument->scopes = $scopeDocuments;
+ return $clientDocument;
}
+ // TODO Add AuthorizeResult or something like that!
/**
* POST authorize data.
*
@@ -288,303 +120,164 @@ public function authorizeGet($request)
*
* @return [type] [description]
*/
- public function authorizePost($request)
+ public function authorizePost()
{
- $postParams = new Set($request->post());
- $params = new Set($request->get());
+ $params = $this->getContainer()->get('parser')->getData()->getParameters();
+ $postParams = $this->getContainer()->get('parser')->getData()->getPayload();
- // CSRF protection
- if (!$postParams->has('csrfToken') || !isset($_SESSION['csrfToken']) || ($postParams->get('csrfToken') !== $_SESSION['csrfToken'])) {
- throw new \Exception('Invalid CSRF token.', Resource::STATUS_BAD_REQUEST);
- }
+ $this->validateCsrf($postParams);
+ $this->validateAction($postParams);
- // TODO: Improve this, load stuff from config, add documented error codes, separate stuff into functions, etc.
if ($postParams->get('action') === 'accept') {
- $expiresAt = time() + 3600;
- $collection = $this->getDocumentManager()->getCollection('oAuthClients');
- $cursor = $collection->find();
- $cursor->where('clientId', $params->get('client_id'));
- $clientDocument = $cursor->current();
- $collection = $this->getDocumentManager()->getCollection('users');
- $userDocument = $collection->getDocument($_SESSION['userId']);
- $collection = $this->getDocumentManager()->getCollection('authScopes');
+ $expiresAt = time() + Config::get(['xAPI', 'oauth', 'token_expiry_time']);
+ // getClientById
+ $clientDocument = $this->getStorage()->getOAuthClientsStorage()->getClientById($params->client_id);
+
+ // getUserById -- $_SESSION['userId']
+ $userDocument = $this->getStorage()->getUserStorage()->findById($_SESSION['userId']);
+
$scopeDocuments = [];
- $scopes = explode(',', $params->get('scope'));
- foreach ($scopes as $scope) {
- $cursor = $collection->find();
- $cursor->where('name', $scope);
- $scopeDocument = $cursor->current();
- if (null === $scopeDocument) {
- throw new \Exception('Invalid scope given!', Resource::STATUS_BAD_REQUEST);
- }
- $scopeDocuments[] = $scopeDocument;
- }
+ $scopes = explode(',', $params->scope);
+ $scopeDocuments = $this->validateAndMapScopes($scopes);// throws exception if not a valid scope
+
$code = Util\OAuth::generateToken();
$token = $this->addToken($expiresAt, $userDocument, $clientDocument, $scopeDocuments, $code);
- $this->token = $token;
$redirectUri = Url::createFromUrl($params->get('redirect_uri'));
$redirectUri->getQuery()->modify(['code' => $token->getCode()]); //We could also use just $code
- $this->redirectUri = $redirectUri;
+ return $redirectUri;
} elseif ($postParams->get('action') === 'deny') {
$redirectUri = Url::createFromUrl($params->get('redirect_uri'));
$redirectUri->getQuery()->modify(['error' => 'User denied authorization!']);
- $this->redirectUri = $redirectUri;
- } else {
- throw new Exception('Invalid.', Resource::STATUS_BAD_REQUEST);
+ return $redirectUri;
}
}
/**
- * @param [type] $request [description]
+ * Validates and retrieves access token
*
- * @return [type] [description]
+ * @return array json document
*/
- public function accessTokenPost($request)
+ public function accessTokenPost()
{
- $params = new Set($request->post());
+ $params = $this->getContainer()->get('parser')->getData()->getPayload();
+ $params = new Util\Collection($params);
$requiredParams = ['grant_type', 'client_id', 'client_secret', 'redirect_uri', 'code'];
- //TODO: Use json-schema validator
- foreach ($requiredParams as $requiredParam) {
- if (!$params->has($requiredParam)) {
- throw new \Exception('Parameter '.$requiredParam.' is missing!', Resource::STATUS_BAD_REQUEST);
- }
- }
-
- if ($params->get('grant_type') !== 'authorization_code') {
- throw new \Exception('Invalid grant_type specified.', Resource::STATUS_BAD_REQUEST);
- }
-
- $collection = $this->getDocumentManager()->getCollection('oAuthTokens');
- $cursor = $collection->find();
-
- $cursor->where('code', $params->get('code'));
- $tokenDocument = $cursor->current();
-
- if (null === $tokenDocument) {
- throw new \Exception('Invalid code specified!', Resource::STATUS_BAD_REQUEST);
- }
-
- $clientDocument = $tokenDocument->client;
+ $this->validateRequiredParams($params, $requiredParams);
+ $this->validateGrantType($params['grant_type']);
- if ($clientDocument->getClientId() !== $params->get('client_id') || $clientDocument->getSecret() !== $params->get('client_secret')) {
- throw new \Exception('Invalid client_id/client_secret combination!', Resource::STATUS_BAD_REQUEST);
- }
-
- if ($params->get('redirect_uri') !== $clientDocument->getRedirectUri()) {
- throw new \Exception('Redirect_uri mismatch!', Resource::STATUS_BAD_REQUEST);
- }
-
- //Remove one-time code
- $tokenDocument->setCode(false);
- $tokenDocument->save();
-
- $this->accessTokens = [$tokenDocument];
- $this->single = true;
+ // getTokenWithOneTimeCode($params)
+ $tokenDocument = $this->getStorage()->getOAuthStorage()->getTokenWithOneTimeCode($params);
return $tokenDocument;
}
public function extractToken(Request $request)
{
- $tokenHeader = $request->headers('Authorization', false);
- $rawTokenHeader = $request->rawHeaders('Authorization', false);
+ $tokenHeader = $request->getHeaderLine('Authorization');
if ($tokenHeader && preg_match('/Bearer\s*([^\s]+)/', $tokenHeader, $matches)) {
$tokenHeader = $matches[1];
- } elseif ($rawTokenHeader && preg_match('/Bearer\s*([^\s]+)/', $rawTokenHeader, $matches)) {
- $tokenHeader = $matches[1];
} else {
$tokenHeader = false;
}
- $tokenRequest = $request->post('access_token', false);
- $tokenQuery = $request->get('access_token', false);
+ $tokenParam = $request->getParam('access_token', false);
// At least one (and only one) of client credentials method required.
- if (!$tokenHeader && !$tokenRequest && !$tokenQuery) {
- throw new Exception('The request is missing a required parameter.', Resource::STATUS_BAD_REQUEST);
- } elseif (($tokenHeader && $tokenRequest) || ($tokenRequest && $tokenQuery) || ($tokenQuery && $tokenHeader)) {
- throw new Exception('The request includes multiple credentials.', Resource::STATUS_BAD_REQUEST);
+ if (!$tokenHeader && !$tokenParam) {
+ throw new AuthFailureException('The request is missing a required parameter.', Controller::STATUS_BAD_REQUEST);
+ } elseif ($tokenHeader && $tokenParam) {
+ throw new AuthFailureException('The request includes multiple credentials.', Controller::STATUS_BAD_REQUEST);
}
$accessToken = $tokenHeader
- ?: $tokenRequest
- ?: $tokenQuery;
+ ?: $tokenParam;
try {
$tokenDocument = $this->fetchToken($accessToken);
} catch (\Exception $e) {
- throw new Exception('Access token invalid.');
+ throw new AuthFailureException('Access token invalid.');
}
- return $tokenDocument;
- }
-
- /**
- * Gets the Access tokens.
- *
- * @return array
- */
- public function getAccessTokens()
- {
- return $this->accessTokens;
- }
-
- /**
- * Sets the Access tokens.
- *
- * @param array $accessTokens the access tokens
- *
- * @return self
- */
- public function setAccessTokens(array $accessTokens)
- {
- $this->accessTokens = $accessTokens;
-
return $this;
}
/**
- * Gets the Cursor.
- *
- * @return cursor
+ * Validates and compiles a document of given scopes
+ * @return array collection of mapo
*/
- public function getCursor()
+ private function validateAndMapScopes($scopes)
{
- return $this->cursor;
- }
+ $auth = $this->getContainer()->get('auth');
+ $scopeDocuments = [];
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor($cursor)
- {
- $this->cursor = $cursor;
+ foreach ($scopes as $scope) {
+ // Get scope by name
+ $scopeDocument = $auth->getAuthScope($scope);
- return $this;
- }
+ if (!$scopeDocument) {
+ throw new Exception('Invalid scope given!', Controller::STATUS_BAD_REQUEST);
+ }
+ $user = $this->getStorage()->getUserStorage()->findById($_SESSION['userId']);
+ if (!in_array($scope, $user->permissions)) {
+ throw new Exception('User does not have enough permissions for requested scope!', Controller::STATUS_BAD_REQUEST);
+ }
+ $scopeDocuments[$scope] = $scopeDocument;
+ }
- /**
- * Gets the Is this a single access token fetch?.
- *
- * @return bool
- */
- public function getSingle()
- {
- return $this->single;
+ return $scopeDocuments;
}
- /**
- * Sets the Is this a single access token fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
+ private function validateCsrf($params)
{
- $this->single = $single;
-
- return $this;
- }
-
- /**
- * Gets the The relevant client(s).
- *
- * @return \API\Document\Auth\OAuthClient
- */
- public function getClient()
- {
- return $this->client;
- }
-
- /**
- * Sets the The relevant client(s).
- *
- * @param \API\Document\Auth\OAuthClient $client the client
- *
- * @return self
- */
- public function setClient(\API\Document\Auth\OAuthClient $client)
- {
- $this->client = $client;
-
- return $this;
+ // CSRF protection
+ if (!isset($params['csrfToken']) || !isset($_SESSION['csrfToken']) || ($params['csrfToken'] !== $_SESSION['csrfToken'])) {
+ throw new Exception('Invalid CSRF token.', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Gets the The relevant scopes.
- *
- * @return array
- */
- public function getScopes()
+ private function validateAction($params)
{
- return $this->scopes;
+ if ($params['action'] !== 'accept' && $params['action'] !== 'deny') {
+ throw new Exception('Invalid.', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the The relevant scopes.
- *
- * @param array $scopes the scopes
- *
- * @return self
- */
- public function setScopes(array $scopes)
+ private function validateRequiredParams($params, $requiredParams)
{
- $this->scopes = $scopes;
-
- return $this;
+ //TODO 0.11.x: Use GraphQL to validate these params
+ foreach ($requiredParams as $requiredParam) {
+ if (!isset($params->{$requiredParam})) {
+ throw new Exception('Parameter '.$requiredParam.' is missing!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
}
- /**
- * Gets the The relevant token.
- *
- * @return \API\Document\Auth\OAuthToken
- */
- public function getToken()
+ private function validateResponseType($responseType)
{
- return $this->token;
+ if ($responseType !== 'code') {
+ throw new \Exception('Invalid response_type specified.', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the The relevant token.
- *
- * @param \API\Document\Auth\OAuthToken $token the token
- *
- * @return self
- */
- public function setToken(\API\Document\Auth\OAuthToken $token)
+ private function validateRedirectUri($redirectUri, $clientDocument)
{
- $this->token = $token;
-
- return $this;
+ if ($redirectUri !== $clientDocument->redirectUri) {
+ throw new \Exception('Redirect_uri mismatch!', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Gets the The relevant redirectUri.
- *
- * @return string
- */
- public function getRedirectUri()
+ private function validateGrantType($grantType)
{
- return $this->redirectUri;
+ if ($grantType !== 'authorization_code') {
+ throw new \Exception('Invalid grant_type specified.', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the The relevant redirectUri.
- *
- * @param string $redirectUri the redirect uri
- *
- * @return self
- */
- public function setRedirectUri($redirectUri)
+ public function validateClientDocument($clientDocument)
{
- $this->redirectUri = $redirectUri;
-
- return $this;
+ if (null === $clientDocument) {
+ throw new \Exception('Invalid client_id', Controller::STATUS_BAD_REQUEST);
+ }
}
}
diff --git a/src/xAPI/Service/AuthScopes.php b/src/xAPI/Service/AuthScopes.php
deleted file mode 100644
index 250a1ab5..00000000
--- a/src/xAPI/Service/AuthScopes.php
+++ /dev/null
@@ -1,174 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Service;
-
-use API\Service;
-use API\Resource;
-
-class AuthScopes extends Service
-{
- private $matrix = [
- 'super' => [
- 'all'
- ],
- 'all' => [
- 'statements/write',
- 'attachments',
- 'state',
- 'profile',
- 'define',
- 'all/read',
- ],
- 'all/read' => [
- 'statements/read',
- 'attachments',
- 'state', //TODO limit to read (service)
- 'profile', //TODO limit to read (service)
- ],
- 'statements/write' => [
- 'statements/read/mine'
- ],
- 'statements/read' => [
- 'statements/read/mine'
- ]
- ];
-
- /**
- * return permission matrix
- * @return int
- */
- public function getMatrix()
- {
- return $this->matrix;
- }
-
- /**
- * return permission matrix
- * @return int
- */
- public function getChildrenFor($name)
- {
- if (!isset($this->matrix[$name])) {
- return [];
- }
-
- $children = $this->matrix[$name];
- foreach ($children as $c) {
- if (in_array($name, $children)) {
- continue;
- }
- $children = array_merge($children, $this->getChildrenFor($c));
- }
-
- return array_unique($children);
- }
-
-
- /**
- * counts permission doocuments
- * @return int
- */
- public function count()
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- $cursor = $collection->find();
- return $cursor->count();
- }
-
- /**
- * Gets all permission documents
- * @param bool $dictionary return as associative array of documents (permission name)
- * @return array of \Sokil\Mongo\Document
- */
- public function fetchAll($dictionary = false)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- $cursor = $collection->find();
- $documents = $cursor->findAll();
-
- if (!$dictionary) {
- return $documents;
- }
-
- $dictionary = [];
- foreach ($cursor as $permission) {
- $dictionary[$permission->getName()] = $permission;
- }
- return $dictionary;
- }
-
- /**
- * Gets a single registered permission document by id
- * @param string $id
- * @return \Sokil\Mongo\Document|null
- */
- public function findById($id)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- return $collection->getDocument($id);
- }
-
- /**
- * Gets a single registered permission document by name
- * @param string $name
- * @return \Sokil\Mongo\Document|null
- */
- public function findByName($name)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- $cursor = $collection->find();
- $cursor->where('name', $name);
- $scopeDocument = $cursor->current();
- return $scopeDocument;
- }
-
- /**
- * Gets a list permissions document by names
- * Note: this function does NOT validate if a given permission name exists.
- *
- * @param array $names
- * @return array of \Sokil\Mongo\Document
- */
- public function findByNames($names, $dictionary = false)
- {
- $collection = $this->getDocumentManager()->getCollection('authScopes');
- $cursor = $collection->find();
-
-
- $cursor->where('name', ['$in' => $names]);
- $documents = $cursor->findAll();
-
- // Force dictionary mode for legacy LRS:
- // Deals with existing LRS suffering from #103: "./X setup:oauth - duplication of scopes when called multiple times"
- // TODO for future release: remove, collection indexing (unique), add migration script
-
- $dictionary = [];
- foreach ($cursor as $permission) {
- $dictionary[$permission->getName()] = $permission;
- }
-
- return ($dictionary) ? $dictionary : array_values($dictionary);
- }
-}
diff --git a/src/xAPI/Service/Exception.php b/src/xAPI/Service/Exception.php
index 6b1a2e4e..cd8dea77 100644
--- a/src/xAPI/Service/Exception.php
+++ b/src/xAPI/Service/Exception.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/xAPI/Service/Log.php b/src/xAPI/Service/Log.php
index 612bb860..e785ca88 100644
--- a/src/xAPI/Service/Log.php
+++ b/src/xAPI/Service/Log.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,7 +30,7 @@
class Log extends Service
{
/**
- * Creates a log entry from the given request
+ * Creates a log entry from the given request.
*
* @param Slim\Http\Request $request The request
*
@@ -38,16 +38,12 @@ class Log extends Service
*/
public function logRequest($request)
{
- $collection = $this->getDocumentManager()->getCollection('logs');
- $document = $collection->createDocument();
-
- $document->setIp($request->getIp());
- $document->setMethod($request->getMethod());
- $document->setEndpoint($request->getPathInfo());
+ $ip = $request->getServerParam('REMOTE_ADDR');
+ $method = $request->getMethod();
+ $target = $request->getRequestTarget();
$currentDate = Util\Date::dateTimeExact();
- $document->setTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
-
- $document->save();
+ $logStorage = $this->getStorage()->getLogStorage();
+ $document = $logStorage->logRequest($ip, $method, $target, $currentDate);
return $document;
}
diff --git a/src/xAPI/Service/Statement.php b/src/xAPI/Service/Statement.php
index fc7e9b63..5f778409 100644
--- a/src/xAPI/Service/Statement.php
+++ b/src/xAPI/Service/Statement.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,403 +25,24 @@
namespace API\Service;
use API\Service;
-use MongoDate;
-use API\Resource;
-use API\Util;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
-use Rhumsaa\Uuid\Uuid;
+use API\HttpException as Exception;
+use API\Config;
+use API\Controller;
class Statement extends Service
{
- /**
- * Statements.
- *
- * @var array
- */
- protected $statements;
-
- /**
- * Attachments.
- *
- * @var array
- */
- protected $attachments;
-
- /**
- * The limit associated with the document query.
- *
- * @var int
- */
- protected $limit;
-
- /**
- * Format associated with the query.
- *
- * @var string
- */
- protected $format;
-
- /**
- * Descending order associated with the query.
- *
- * @var bool
- */
- protected $descending;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single statement fetch?
- *
- * @var bool
- */
- protected $single = false;
-
- /**
- * Is this a single statement match?
- *
- * @var bool
- */
- protected $match = false;
-
- /**
- * Provide a statement count.
- *
- * @var int
- */
- protected $count;
-
/**
* Fetches statements according to the given parameters.
*
- * @param array $request The HTTP request object.
- *
* @return array An array of statement objects.
*/
- public function statementGet($request)
+ public function statementGet()
{
- $params = new Set($request->get());
-
- $collection = $this->getDocumentManager()->getCollection('statements');
- $cursor = $collection->find();
+ $parameters = $this->getContainer()->get('parser')->getData()->getParameters();
- // Check if the user only has statements/read/mine permission and nothing more
- // In which case we restrict him severely
- if (
- $this->getAccessToken()->hasPermission('statements/read/mine')
- && !$this->getAccessToken()->hasPermission('statements/read')
- && !$this->getAccessToken()->hasPermission('all')
- && !$this->getAccessToken()->hasPermission('all/read')
- && !$this->getAccessToken()->hasPermission('super')) {
-
- // Check exact match for authority
- //$cursor->where('statement.authority', $this->getAccessToken()->generateAuthority());
-
- // Check that token belongs to same user (but can be different token)
- // Example - user has a basic and OAuth token
- // He writes statements with one of them and reads them with the other one
- // However, e-mail uniqueness must be ensured for this to work
- $cursor->whereOr(
- // OAuth token - NOTE: This might be statement.authority.1.member.mbox or
- // statement.authority.{}.member.mbox
- //$collection->expression()->where('statement.authority.member.mbox', $this->getAccessToken()->user->getEmail()),
- $collection->expression()->whereElemMatch('statement.authority.member',
- $collection->expression()
- ->where('mbox', 'mailto:' . $this->getAccessToken()->user->getEmail())
- ),
- // Basic token
- $collection->expression()->where('statement.authority.account.name', $this->getAccessToken()->user->getEmail())
- );
-
-
- }
-
- // Single statement
- if ($params->has('statementId')) {
- $cursor->where('statement.id', $params->get('statementId'));
- $cursor->where('voided', false);
-
- if(!Uuid::isValid($params->get('statementId'))){
- throw new Exception('Not a valid uuid.', Resource::STATUS_NOT_FOUND);
- }
- if ($cursor->count() === 0) {
- throw new Exception('Statement does not exist.', Resource::STATUS_NOT_FOUND);
- }
-
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
-
- if ($params->has('voidedStatementId')) {
- $cursor->where('statement.id', $params->get('voidedStatementId'));
- $cursor->where('voided', true);
-
- if ($cursor->count() === 0) {
- throw new Exception('Statement does not exist.', Resource::STATUS_NOT_FOUND);
- }
-
- $this->cursor = $cursor;
- $this->single = true;
-
- return $this;
- }
-
- $cursor->where('voided', false);
-
- // Multiple statements
- if ($params->has('agent')) {
- $agent = $params->get('agent');
- $agent = json_decode($agent, true);
- //Fetch the identifier - otherwise we'd have to order the JSON
- if (isset($agent['mbox'])) {
- $uniqueIdentifier = 'mbox';
- } elseif (isset($agent['mbox_sha1sum'])) {
- $uniqueIdentifier = 'mbox_sha1sum';
- } elseif (isset($agent['openid'])) {
- $uniqueIdentifier = 'openid';
- } elseif (isset($agent['account'])) {
- $uniqueIdentifier = 'account';
- }
- if ($params->has('related_agents') && $params->get('related_agents') === 'true') {
- if ($uniqueIdentifier === 'account') {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.authority.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.authority.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.context.team.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.context.team.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.context.instructor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.context.instructor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.object.objectType', 'SubStatement'),
- $collection->expression()->where('statement.object.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.object.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.authority.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.authority.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.context.team.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.context.team.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.context.instructor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.context.instructor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.object.objectType', 'SubStatement'),
- $collection->expression()->where('references.object.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.object.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- )
- )
- );
- } else {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('statement.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('statement.authority.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('statement.context.team.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('statement.context.instructor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.object.objectType', 'SubStatement'),
- $collection->expression()->where('statement.object.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
- ),
- $collection->expression()->where('references.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.authority.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.context.team.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.context.instructor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.object.objectType', 'SubStatement'),
- $collection->expression()->where('references.object.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
- )
- )
- );
- }
- } else {
- if ($uniqueIdentifier === 'account') {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('statement.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- ),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
- $collection->expression()->where('references.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
- )
- )
- );
- } else {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('statement.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
- $collection->expression()->where('references.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
- )
- );
- }
- }
- }
- if ($params->has('verb')) {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.verb.id', $params->get('verb')),
- $collection->expression()->where('references.verb.id', $params->get('verb'))
- )
- );
- }
- if ($params->has('activity')) {
- // Handle related
- if ($params->has('related_activities') && $params->get('related_activities') === 'true') {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.object.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.category.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.grouping.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.other.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->where('statement.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->whereAnd(
- $collection->expression()->where('statement.object.objectType', 'SubStatement'),
- $collection->expression()->where('statement.object.object', $params->get('activity'))
- ),
- $collection->expression()->where('references.object.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.category.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.grouping.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.other.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->where('references.context.contextActivities.parent.id', $params->get('activity')),
- $collection->expression()->whereAnd(
- $collection->expression()->where('references.object.objectType', 'SubStatement'),
- $collection->expression()->where('references.object.object', $params->get('activity'))
- )
- )
- );
- } else {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.object.id', $params->get('activity')),
- $collection->expression()->where('references.object.id', $params->get('activity'))
- )
- );
- }
- }
-
- if ($params->has('registration')) {
- $cursor->whereAnd(
- $collection->expression()->whereOr(
- $collection->expression()->where('statement.context.registration', $params->get('registration')),
- $collection->expression()->where('references.context.registration', $params->get('registration'))
- )
- );
- }
-
- // Date based filters
- if ($params->has('since')) {
- $date = Util\Date::dateRFC3339($params->get('since'));
- if(!$date){
- throw new Exception('"since" parameter is not a valid ISO 8601 timestamp.(Good example: 2015-11-18T12:17:00+00:00), ', Resource::STATUS_NOT_FOUND);
- }
- $since = Util\Date::dateTimeToMongoDate($date);
- $cursor->whereGreaterOrEqual('mongo_timestamp', $since);
- }
-
- if ($params->has('until')) {
- $date = Util\Date::dateRFC3339($params->get('until'));
- if(!$date){
- throw new Exception('"until" parameter is not a valid ISO 8601 timestamp.(Good example: 2015-11-18T12:17:00+00:00), ', Resource::STATUS_NOT_FOUND);
- }
- $until = Util\Date::dateTimeToMongoDate($date);
- $cursor->whereLessOrEqual('mongo_timestamp', $until);
- }
-
- // Count before paginating
- $this->count = $cursor->count();
-
- // Handle pagination
- if ($params->has('since_id')) {
- $id = new \MongoId($params->get('since_id'));
- $cursor->whereGreaterOrEqual('_id', $id);
- }
+ $statementResult = $this->getStorage()->getStatementStorage()->get($parameters);
- if ($params->has('until_id')) {
- $id = new \MongoId($params->get('until_id'));
- $cursor->whereLessOrEqual('_id', $id);
- }
-
- $this->format = $this->getSlim()->config('xAPI')['default_statement_get_format'];
- if ($params->has('format')) {
- $this->format = $params->get('format');
- }
-
- $this->descending = true;
- $cursor->sort(['_id' => -1]);
- if ($params->has('ascending')) {
- $asc = $params->get('ascending');
- if(strtolower($asc) === 'true' || $asc === '1') {
- $cursor->sort(['_id' => 1]);
- $this->descending = false;
- }
- }
-
- if ($params->has('limit') && $params->get('limit') < $this->getSlim()->config('xAPI')['statement_get_limit'] && $params->get('limit') > 0) {
- $limit = $params->get('limit');
- } else {
- $limit = $this->getSlim()->config('xAPI')['statement_get_limit'];
- }
- // Hackish solution...think of a different way for handling this
- $limit = $limit + 1;
-
- $this->limit = $limit;
- $cursor->limit($limit);
-
- $this->cursor = $cursor;
-
- return $this;
+ return $statementResult;
}
/**
@@ -429,47 +50,18 @@ public function statementGet($request)
*
* @return array An array of statement documents or a single statement document.
*/
- public function statementPost($request)
+ public function statementPost()
{
- // Check for multipart request
- if ($request->isMultipart()) {
- $jsonRequest = $request->parts()->get(0);
- } else {
- $jsonRequest = $request;
- }
-
- // TODO: Move header validation in json-schema as well
- if ($jsonRequest->getMediaType() !== 'application/json') {
- throw new \Exception('Media type specified in Content-Type header must be \'application/json\'!', Resource::STATUS_BAD_REQUEST);
- }
-
- // Validation has been completed already - everyhing is assumed to be valid
- $body = $jsonRequest->getBody();
- $body = json_decode($body, true);
-
- // Some clients escape the JSON - handle them
- if (is_string($body)) {
- $body = json_decode($body, true);
- }
+ $this->validateJsonMediaType($this->getContainer()->get('parser')->getData());
- $collection = $this->getDocumentManager()->getCollection('statements');
- $activityCollection = $this->getDocumentManager()->getCollection('activities');
+ if (count($this->getContainer()->get('parser')->getAttachments()) > 0) {
+ $fsAdapter = \API\Util\Filesystem::generateAdapter(Config::get('filesystem'));
- // Save attachments - this could be in a queue perhaps...
- if ($request->isMultipart()) {
- $fsAdapter = \API\Util\Filesystem::generateAdapter($this->getSlim()->config('filesystem'));
-
- $attachmentCollection = $this->getDocumentManager()->getCollection('attachments');
-
- $partCount = $request->parts()->count();
-
- for ($i = 1; $i < $partCount; $i++) {
- $part = $request->parts()->get($i);
-
- $attachmentBody = $part->getBody();
+ foreach ($this->getContainer()->get('parser')->getAttachments() as $attachment) {
+ $attachmentBody = $attachment->getRawPayload();
$detectedEncoding = mb_detect_encoding($attachmentBody);
- $contentEncoding = $part->headers('Content-Transfer-Encoding');
+ $contentEncoding = isset($attachment->getHeaders()['content-transfer-encoding']) ? $attachment->getHeaders()['content-transfer-encoding'][0] : null;
if ($detectedEncoding === 'UTF-8' && ($contentEncoding === null || $contentEncoding === 'binary')) {
try {
@@ -479,170 +71,27 @@ public function statementPost($request)
}
}
- $hash = $part->headers('X-Experience-API-Hash');
- $contentType = $part->headers('Content-Type');
+ $this->validateAttachmentRequest($attachment);
+ $hash = $attachment->getHeaders()['x-experience-api-hash'][0];
+ $contentType = $attachment->getHeaders()['content-type'][0];
- $attachmentDocument = $attachmentCollection->createDocument();
- $attachmentDocument->setSha2($hash);
- $attachmentDocument->setContentType($contentType);
- $attachmentDocument->setTimestamp(new MongoDate());
- $attachmentDocument->save();
+ $this->getStorage()->getAttachmentStorage()->store($hash, $contentType);
$fsAdapter->put($hash, $attachmentBody);
}
}
- $attachmentBase = $this->getSlim()->url->getBaseUrl().$this->getSlim()->config('filesystem')['exposed_url'];
+ $body = $this->getContainer()->get('parser')->getData()->getPayload();
// Multiple statements
if ($this->areMultipleStatements($body)) {
- $statements = [];
- foreach ($body as $statement) {
- if (isset($statement['id'])) {
- $cursor = $collection->find();
- $cursor->where('statement.id', $statement['id']);
- $result = $cursor->findOne();
-
- // ID exists, check if different or conflict
- if ($result) {
- // Same - return 200
- if ($statement == $result->getStatement()) {
- $this->match = true;
- } else { // Mismatch - return 409 Conflict
- throw new Exception('An existing statement already exists with the same ID and is different from the one provided.', Resource::STATUS_CONFLICT);
- }
- }
- }
-
- $statementDocument = $collection->createDocument();
- // Overwrite authority - unless it's a super token and manual authority is set
- if (!($this->getAccessToken()->isSuperToken() && isset($statement['authority'])) || !isset($statement['authority'])) {
- $statement['authority'] = $this->getAccessToken()->generateAuthority();
- }
- $statementDocument->setStatement($statement);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $statementDocument->setStored(Util\Date::dateTimeToISO8601($currentDate));
- $statementDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $statementDocument->setDefaultTimestamp();
- $statementDocument->fixAttachmentLinks($attachmentBase);
- $statementDocument->convertExtensionKeysToUnicode();
- $statementDocument->setDefaultId();
- $statementDocument->legacyContextActivities();
- if ($statementDocument->isReferencing()) {
- // Copy values of referenced statement chain inside current statement for faster query-ing
- // (space-time tradeoff)
- $referencedStatement = $statementDocument->getReferencedStatement();
-
- $existingReferences = [];
- if (null !== $referencedStatement->getReferences()) {
- $existingReferences = $referencedStatement->getReferences();
- }
- $existingReferences[] = $referencedStatement->getStatement();
- $statementDocument->setReferences($existingReferences);
- }
- $statements[] = $statementDocument->toArray();
- $this->statements[] = $statementDocument;
- if ($statementDocument->isVoiding()) {
- $referencedStatement = $statementDocument->getReferencedStatement();
- if (!$referencedStatement->isVoiding()) {
- $referencedStatement->setVoided(true);
- $referencedStatement->save();
- } else {
- throw new \Exception('Voiding statements cannot be voided.', Resource::STATUS_CONFLICT);
- }
- }
- if ($this->getAccessToken()->hasPermission('define')) {
- $activities = $statementDocument->extractActivities();
- if (count($activities) > 0) {
- $activityCollection->insertMultiple($activities);
- }
- }
- // Save statement
- $statementDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('statements', $statementDocument)->save();
- }
- // $collection->insertMultiple($statements); // Batch operation is much faster ~600%
- // However, because we add every single statement to the access log, we can't use it
- // The only way to still use (fast) batch inserts would be to move the attachment of
- // statements to their respective log entries in a async queue!
+ $statementResult = $this->getStorage()->getStatementStorage()->insertMultiple($body);
} else {
// Single statement
- if (isset($body['id'])) {
- $cursor = $collection->find();
- $cursor->where('statement.id', $body['id']);
- $result = $cursor->findOne();
-
- // ID exists, check if different or conflict
- if ($result) {
- // Same - return 200
- if ($body == $result->getStatement()) {
- $this->match = true;
- } else { // Mismatch - return 409 Conflict
- throw new Exception('An existing statement already exists with the same ID and is different from the one provided.', Resource::STATUS_CONFLICT);
- }
- }
- }
-
- $statementDocument = $collection->createDocument();
- // Overwrite authority - unless it's a super token and manual authority is set
- if (!($this->getAccessToken()->isSuperToken() && isset($body['authority'])) || !isset($body['authority'])) {
- $body['authority'] = $this->getAccessToken()->generateAuthority();
- }
- $statementDocument->setStatement($body);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $statementDocument->setStored(Util\Date::dateTimeToISO8601($currentDate));
- $statementDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $statementDocument->setDefaultTimestamp();
- $statementDocument->fixAttachmentLinks($attachmentBase);
- $statementDocument->convertExtensionKeysToUnicode();
- $statementDocument->setDefaultId();
- $statementDocument->legacyContextActivities();
-
- if ($statementDocument->isReferencing()) {
- // Copy values of referenced statement chain inside current statement for faster query-ing
- // (space-time tradeoff)
- $referencedStatement = $statementDocument->getReferencedStatement();
-
- $existingReferences = [];
- if (null !== $referencedStatement->getReferences()) {
- $existingReferences = $referencedStatement->getReferences();
- }
- $existingReferences[] = $referencedStatement->getStatement();
-
- $statementDocument->setReferences($existingReferences);
- }
-
- if ($statementDocument->isVoiding()) {
- $referencedStatement = $statementDocument->getReferencedStatement();
- if (!$referencedStatement->isVoiding()) {
- $referencedStatement->setVoided(true);
- $referencedStatement->save();
- } else {
- throw new \Exception('Voiding statements cannot be voided.', Resource::STATUS_CONFLICT);
- }
- }
-
- if ($this->getAccessToken()->hasPermission('define')) {
- $activities = $statementDocument->extractActivities();
- if (count($activities) > 0) {
- $activityCollection->insertMultiple($activities);
- }
- }
-
- $statementDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('statements', $statementDocument)->save();
-
- $this->single = true;
- $this->statements = [$statementDocument];
+ $statementResult = $this->getStorage()->getStatementStorage()->insertOne($body);
}
- return $this;
+ return $statementResult;
}
/**
@@ -650,397 +99,69 @@ public function statementPost($request)
*
* @return
*/
- public function statementPut($request)
+ public function statementPut()
{
- // Check for multipart request
- if ($request->isMultipart()) {
- $jsonRequest = $request->parts()->get(0);
- } else {
- $jsonRequest = $request;
- }
-
-
- // Validation has been completed already - everyhing is assumed to be valid (from an external view!)
- // TODO: Move header validation in json-schema as well
- if ($jsonRequest->getMediaType() !== 'application/json') {
- throw new \Exception('Media type specified in Content-Type header must be \'application/json\'!', Resource::STATUS_BAD_REQUEST);
- }
-
- // Validation has been completed already - everyhing is assumed to be valid
- $body = $jsonRequest->getBody();
- $body = json_decode($body, true);
-
- // Some clients escape the JSON - handle them
- if (is_string($body)) {
- $body = json_decode($body, true);
- }
-
- // Save attachments - this could be in a queue perhaps...
- if ($request->isMultipart()) {
- $fsAdapter = \API\Util\Filesystem::generateAdapter($this->getSlim()->config('filesystem'));
+ $this->validateJsonMediaType($this->getContainer()->get('parser')->getData());
- $attachmentCollection = $this->getDocumentManager()->getCollection('attachments');
+ if (count($this->getContainer()->get('parser')->getAttachments()) > 0) {
+ $fsAdapter = \API\Util\Filesystem::generateAdapter(Config::get('filesystem'));
- $partCount = $request->parts()->count();
-
- for ($i = 1; $i < $partCount; $i++) {
- $part = $request->parts()->get($i);
-
- $attachmentBody = $part->getBody();
+ foreach ($this->getContainer()->get('parser')->getAttachments() as $attachment) {
+ $attachmentBody = $attachment->getRawPayload();
$detectedEncoding = mb_detect_encoding($attachmentBody);
- $contentEncoding = $part->headers('Content-Transfer-Encoding');
+ $contentEncoding = isset($attachment->getHeaders()['content-transfer-encoding']) ? $attachment->getHeaders()['content-transfer-encoding'][0] : null;
if ($detectedEncoding === 'UTF-8' && ($contentEncoding === null || $contentEncoding === 'binary')) {
try {
$attachmentBody = iconv('UTF-8', 'ISO-8859-1//IGNORE', $attachmentBody);
} catch (\Exception $e) {
- //Use raw file on failed conversion (do nothing!)
+ // Use raw file on failed conversion (do nothing!)
}
}
- $hash = $part->headers('X-Experience-API-Hash');
- $contentType = $part->headers('Content-Type');
+ $hash = $attachment->getHeaders()['X-Experience-API-Hash'];
+ $contentType = $part->getHeaders()['Content-Type'];
- $attachmentDocument = $attachmentCollection->createDocument();
- $attachmentDocument->setSha2($hash);
- $attachmentDocument->setContentType($contentType);
- $attachmentDocument->setTimestamp(new MongoDate());
- $attachmentDocument->save();
+ $this->getStorage()->getAttachmentStorage()->store($hash, $contentType);
$fsAdapter->put($hash, $attachmentBody);
}
}
- $attachmentBase = $this->getSlim()->url->getBaseUrl().$this->getSlim()->config('filesystem')['exposed_url'];
-
-
// Single
- $params = new Set($request->get());
-
- $activityCollection = $this->getDocumentManager()->getCollection('activities');
- $collection = $this->getDocumentManager()->getCollection('statements');
- $cursor = $collection->find();
-
- // Check statementId exists
- if (!$params->has('statementId')) {
- throw new Exception('The statementId parameter is missing!', Resource::STATUS_BAD_REQUEST);
- }
-
- // Check statementId is acutally valid
- if(!Uuid::isValid($params->get('statementId'))){
- throw new Exception('The provided statement ID is invalid!', Resource::STATUS_BAD_REQUEST);
- }
-
- // Single statement
- $cursor->where('statement.id', $params->get('statementId'));
- $result = $cursor->findOne();
-
- // Check statementId
- if (isset($body['id'])) {
- // Check for match
- if ($body['id'] !== $params->get('statementId')) {
- throw new \Exception('Statement ID query parameter doesn\'t match the given statement property', Resource::STATUS_BAD_REQUEST);
- }
- } else {
- $body['id'] = $params->get('statementId');
- }
-
- // ID exists, check if different or conflict
- if ($result) {
- // Same - return 204 No content
- if ($body == $result->getStatement()) {
- $this->match = true;
- } else { // Mismatch - return 409 Conflict
- throw new Exception('An existing statement already exists with the same ID and is different from the one provided.', Resource::STATUS_CONFLICT);
- }
- } else { // Store new statement
- $statementDocument = $collection->createDocument();
- // Overwrite authority - unless it's a super token and manual authority is set
- if (!($this->getAccessToken()->isSuperToken() && isset($body['authority'])) || !isset($body['authority'])) {
- $body['authority'] = $this->getAccessToken()->generateAuthority();
- }
-
- // Set the statement
- $statementDocument->setStatement($body);
- // Dates
- $currentDate = Util\Date::dateTimeExact();
- $statementDocument->setStored(Util\Date::dateTimeToISO8601($currentDate));
- $statementDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
- $statementDocument->setDefaultTimestamp();
- $statementDocument->fixAttachmentLinks($attachmentBase);
- $statementDocument->legacyContextActivities();
-
- if ($statementDocument->isReferencing()) {
- // Copy values of referenced statement chain inside current statement for faster query-ing
- // (space-time tradeoff)
- $referencedStatement = $statementDocument->getReferencedStatement();
-
- $existingReferences = [];
- if (null !== $referencedStatement->getReferences()) {
- $existingReferences = $referencedStatement->getReferences();
- }
- $statementDocument->setReferences(array_push($existingReferences, $referencedStatement->getStatement()));
- }
-
- if ($statementDocument->isVoiding()) {
- $referencedStatement = $statementDocument->getReferencedStatement();
- if (!$referencedStatement->isVoiding()) {
- $referencedStatement->setVoided(true);
- $referencedStatement->save();
- } else {
- throw new \Exception('Voiding statements cannot be voided.', Resource::STATUS_CONFLICT);
- }
- }
-
- if ($this->getAccessToken()->hasPermission('define')) {
- $activities = $statementDocument->extractActivities();
- if (count($activities) > 0) {
- $activityCollection->insertMultiple($activities);
- }
- }
-
- $statementDocument->save();
-
- // Add to log
- $this->getSlim()->requestLog->addRelation('statements', $statementDocument)->save();
-
- $this->single = true;
- $this->statements = [$statementDocument];
- }
-
- return $this;
- }
-
- /**
- * Gets the Statements.
- *
- * @return array
- */
- public function getStatements()
- {
- return $this->statements;
- }
-
- /**
- * Sets the Statements.
- *
- * @param array $statements the statements
- *
- * @return self
- */
- public function setStatements(array $statements)
- {
- $this->statements = $statements;
+ $parameters = $this->getContainer()->get('parser')->getData()->getParameters();
+ $body = $this->getContainer()->get('parser')->getData()->getPayload();
- return $this;
- }
-
- /**
- * Gets the Attachments.
- *
- * @return array
- */
- public function getAttachments()
- {
- return $this->attachments;
- }
-
- /**
- * Sets the Attachments.
- *
- * @param array $attachments the attachments
- *
- * @return self
- */
- public function setAttachments(array $attachments)
- {
- $this->attachments = $attachments;
-
- return $this;
- }
-
- /**
- * Gets the The limit associated with the document query.
- *
- * @return int
- */
- public function getLimit()
- {
- return $this->limit;
- }
-
- /**
- * Sets the The limit associated with the document query.
- *
- * @param int $limit the limit
- *
- * @return self
- */
- public function setLimit($limit)
- {
- $this->limit = $limit;
+ $statementResult = $this->getStorage()->getStatementStorage()->put($parameters, $body);
- return $this;
+ return $statementResult;
}
- /**
- * Gets the Format associated with the query.
- *
- * @return string
- */
- public function getFormat()
- {
- return $this->format;
- }
-
- /**
- * Sets the Format associated with the query.
- *
- * @param string $format the format
- *
- * @return self
- */
- public function setFormat($format)
- {
- $this->format = $format;
-
- return $this;
- }
-
- /**
- * Gets the Descending order associated with the query.
- *
- * @return bool
- */
- public function getDescending()
- {
- return $this->descending;
- }
-
- /**
- * Sets the Descending order associated with the query.
- *
- * @param bool $descending the descending
- *
- * @return self
- */
- public function setDescending($descending)
- {
- $this->descending = $descending;
-
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single statement fetch?.
- *
- * @return bool
- */
- public function getSingle()
- {
- return $this->single;
- }
-
- /**
- * Sets the Is this a single statement fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
- {
- $this->single = $single;
-
- return $this;
- }
-
- // Quickest solution for checking 1D vs 2D assoc arrays
+ // Quickest solution for validateing 1D vs 2D assoc arrays
private function areMultipleStatements(&$array)
{
- return ($array === array_values($array));
+ // Is this an array of objects or a single object?
+ return is_array($array);
}
- /**
- * Gets the Is this a single statement match?.
- *
- * @return bool
- */
- public function getMatch()
+ private function validateJsonMediaType($jsonRequest)
{
- return $this->match;
- }
-
- /**
- * Sets the Is this a single statement match?.
- *
- * @param bool $match the is match
- *
- * @return self
- */
- public function setMatch($match)
- {
- $this->match = $match;
-
- return $this;
- }
-
- /**
- * Gets the Access token to check for permissions.
- *
- * @return API\Document\Auth\AbstractToken
- */
- public function getAccessToken()
- {
- return $this->getSlim()->auth;
- }
-
- /**
- * Gets the Provide a statement count.
- *
- * @return int
- */
- public function getCount()
- {
- return $this->count;
+ // TODO 0.11.x: Possibly validate this using GraphQL
+ if (strpos($jsonRequest->getHeaders()['content-type'][0], 'application/json') !== 0) {
+ throw new Exception('Media type specified in Content-Type header must be \'application/json\'!', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the Provide a statement count.
- *
- * @param int $count the count
- *
- * @return self
- */
- public function setCount($count)
+ private function validateAttachmentRequest($attachmentRequest)
{
- $this->count = $count;
+ // TODO 0.11.x: Possibly validate this using GraphQL
+ if (!isset($attachmentRequest->getHeaders()['x-experience-api-hash']) || (empty($attachmentRequest->getHeaders()['x-experience-api-hash']))) {
+ throw new Exception('Missing X-Experience-API-Hash on attachment!', Controller::STATUS_BAD_REQUEST);
+ }
- return $this;
+ if (!isset($attachmentRequest->getHeaders()['content-type']) || (empty($attachmentRequest->getHeaders()['content-type']))) {
+ throw new Exception('Missing Content-Type on attachment!', Controller::STATUS_BAD_REQUEST);
+ }
}
}
diff --git a/src/xAPI/Service/User.php b/src/xAPI/Service/User.php
index ffb832d3..3e98de22 100644
--- a/src/xAPI/Service/User.php
+++ b/src/xAPI/Service/User.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,36 +25,13 @@
namespace API\Service;
use API\Service;
-use API\Resource;
+use API\Controller;
use API\Util\OAuth;
-use API\Util\Rememberme\MongoStorage as RemembermeMongoStorage;
-use Slim\Helper\Set;
-use Sokil\Mongo\Cursor;
-use Birke\Rememberme;
+use API\HttpException as Exception;
+use API\Util\Collection;
class User extends Service
{
- /**
- * Users.
- *
- * @var array
- */
- protected $users;
-
- /**
- * Cursor.
- *
- * @var cursor
- */
- protected $cursor;
-
- /**
- * Is this a single user fetch?
- *
- * @var bool
- */
- protected $single = false;
-
/**
* Any errors that might've ocurred are stored here.
*
@@ -67,7 +44,7 @@ class User extends Service
*
* @return \API\Document\User The user document
*/
- public function loginGet($request)
+ public function loginGet()
{
// CSRF protection
$_SESSION['csrfToken'] = OAuth::generateCsrfToken();
@@ -78,76 +55,33 @@ public function loginGet($request)
*
* @return \API\Document\User The user document
*/
- public function loginPost($request)
+ public function loginPost()
{
- $params = new Set($request->post());
+ $parameters = (object)$this->getContainer()->get('parser')->getData()->getPayload();
- // CSRF protection
- if (!$params->has('csrfToken') || !isset($_SESSION['csrfToken']) || ($params->get('csrfToken') !== $_SESSION['csrfToken'])) {
- throw new \Exception('Invalid CSRF token.', Resource::STATUS_BAD_REQUEST);
- }
-
- // This could be in JSON schema as well :)
- if (!$params->has('email') || !$params->has('password')) {
- throw new \Exception('Username or password missing!', Resource::STATUS_BAD_REQUEST);
- }
-
- $collection = $this->getDocumentManager()->getCollection('users');
- $cursor = $collection->find();
+ $this->validateCsrf($parameters);
+ $this->validateRequiredParameters($parameters);
- $cursor->where('email', $params->get('email'));
- $cursor->where('passwordHash', sha1($params->get('password')));
-
- $document = $cursor->current();
+ $document = $this->getStorage()->getUserStorage()->findByEmailAndPassword($parameters->email, $parameters->password);
if (null === $document) {
$errorMessage = 'Invalid login attempt. Try again!';
$this->errors[] = $errorMessage;
- throw new \Exception($errorMessage, Resource::STATUS_UNAUTHORIZED);
+ throw new \Exception($errorMessage, Controller::STATUS_UNAUTHORIZED);
}
- $this->single = true;
- $this->users = [$document];
-
- // Set the session
- $_SESSION['userId'] = $document->getId();
+ // Set current user auth
+ $_SESSION['userId'] = (string)$document->_id;
$_SESSION['expiresAt'] = time() + 3600; //1 hour
- // Set the Remember me cookie
- $rememberMeStorage = new RemembermeMongoStorage($this->getDocumentManager());
- $rememberMe = new Rememberme\Authenticator($rememberMeStorage);
-
-
- if ($params->has('rememberMe')) {
- $rememberMe->createCookie($document->getId());
- } else {
- $rememberMe->clearCookie();
- }
-
return $document;
}
public function loggedIn()
{
- $rememberMeStorage = new RemembermeMongoStorage($this->getDocumentManager());
- $rememberMe = new Rememberme\Authenticator($rememberMeStorage);
-
if (isset($_SESSION['userId']) && isset($_SESSION['expiresAt']) && $_SESSION['expiresAt'] > time()) {
$_SESSION['expiresAt'] = time() + 3600; //Renew session on every activity
return true;
- } elseif (!empty($_COOKIE[$rememberMe->getCookieName()]) && $rememberMe->cookieIsValid()) { // Remember me cookie
- $loginresult = $rememberMe->login();
- if ($loginresult) {
- // Load user into session and return true
- // Set the session
- $_SESSION['userId'] = $loginresult;
- $_SESSION['expiresAt'] = time() + 3600; //1 hour
- $_SESSION['rememberedByCookie'] = true;
- } else {
- if ($rememberMe->loginTokenWasInvalid()) {
- throw new \Exception('Remember me cookie invalid!', Resource::STATUS_BAD_REQUEST);
- }
- }
} else {
return false;
}
@@ -155,174 +89,58 @@ public function loggedIn()
public function findById($id)
{
- $collection = $this->getDocumentManager()->getCollection('users');
-
- $result = $collection->getDocument($id);
-
- return $result;
- }
-
- public function findByEmail($email)
- {
- $collection = $this->getDocumentManager()->getCollection('users');
- $cursor = $collection->find();
- $cursor->where('email', $email);
-
- return $cursor;
- }
-
- public function getEmailCount($email)
- {
- $collection = $this->getDocumentManager()->getCollection('users');
- $cursor = $collection->find();
- $cursor->where('email', $email);
-
- $count = $cursor->count();
+ $document = $this->getStorage()->getUserStorage()->findById($id);
- return $count;
+ return $document;
}
public function getLoggedIn()
{
$userId = $_SESSION['userId'];
$userDocument = $this->findById($userId);
-
return $userDocument;
}
- public function addUser($email, $password, $permissions)
+ public function addUser($name, $description, $email, $password, $permissions)
{
- $collection = $this->getDocumentManager()->getCollection('users');
-
- // Set up the User to be saved
- $userDocument = $collection->createDocument();
-
- $userDocument->setEmail($email);
-
- $passwordHash = sha1($password);
- $userDocument->setPasswordHash($passwordHash);
-
- foreach ($permissions as $permission) {
- $userDocument->addPermission($permission);
- }
-
- $userDocument->save();
-
- $this->single = true;
- $this->users = [$userDocument];
+ $userDocument = $this->getStorage()->getUserStorage()->addUser($name, $description, $email, $password, $permissions);
return $userDocument;
}
public function fetchAll()
{
- $collection = $this->getDocumentManager()->getCollection('users');
- $cursor = $collection->find();
+ $documentResult = $userDocument = $this->getStorage()->getUserStorage()->fetchAll();
- $this->cursor = $cursor;
-
- return $this;
+ return $documentResult;
}
- public function fetchAvailablePermissions()
+ public function hasEmail($email)
{
- $service = new AuthScopes($this->getSlim());
- return $service->fetchAll();
+ return $this->getStorage()->getUserStorage()->hasEmail($email);
}
- /**
- * Gets the Users.
- *
- * @return array
- */
- public function getUsers()
- {
- return $this->users;
- }
-
- /**
- * Sets the Users.
- *
- * @param array $users the users
- *
- * @return self
- */
- public function setUsers(array $users)
- {
- $this->users = $users;
-
- return $this;
- }
-
- /**
- * Gets the Cursor.
- *
- * @return cursor
- */
- public function getCursor()
- {
- return $this->cursor;
- }
-
- /**
- * Sets the Cursor.
- *
- * @param cursor $cursor the cursor
- *
- * @return self
- */
- public function setCursor(Cursor $cursor)
- {
- $this->cursor = $cursor;
-
- return $this;
- }
-
- /**
- * Gets the Is this a single user fetch?.
- *
- * @return bool
- */
- public function getSingle()
+ private function validateCsrf($params)
{
- return $this->single;
+ // CSRF protection
+ if (!isset($params->csrfToken) || !isset($_SESSION['csrfToken']) || ($params->csrfToken !== $_SESSION['csrfToken'])) {
+ throw new Exception('Invalid CSRF token.', Controller::STATUS_BAD_REQUEST);
+ }
}
- /**
- * Sets the Is this a single user fetch?.
- *
- * @param bool $single the is single
- *
- * @return self
- */
- public function setSingle($single)
+ private function validateRequiredParameters($params)
{
- $this->single = $single;
-
- return $this;
+ // This could be in JSON schema as well :)
+ if (!isset($params->email) || !isset($params->password)) {
+ throw new Exception('Username or password missing!', Controller::STATUS_BAD_REQUEST);
+ }
}
/**
- * Gets the Any errors that might've ocurred are stored here.
- *
* @return array
*/
public function getErrors()
{
return $this->errors;
}
-
- /**
- * Sets the Any errors that might've ocurred are stored here.
- *
- * @param array $errors the errors
- *
- * @return self
- */
- public function setErrors(array $errors)
- {
- $this->errors = $errors;
-
- return $this;
- }
}
diff --git a/src/xAPI/Storage/Adapter/Mongo.php b/src/xAPI/Storage/Adapter/Mongo.php
new file mode 100644
index 00000000..7630d49b
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo.php
@@ -0,0 +1,494 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter;
+
+use MongoDB\Driver\Command;
+use MongoDB\Driver\Cursor;
+
+use API\DocumentInterface;
+use API\BaseTrait;
+use API\Config;
+use API\Storage\AdapterInterface;
+
+use API\Storage\AdapterException;
+use MongoDB\Driver\Exception\Exception as MongoException;
+
+class Mongo implements AdapterInterface
+{
+ use BaseTrait;
+
+ private $client;
+
+ private $databaseName;
+
+ /**
+ * @var self::REQUIRED_DB_VERSION minimal mongo version
+ * @see https://www.mongodb.com/support-policy
+ */
+ const REQUIRED_DB_VERSION = '3.0.0';
+
+ /**
+ * Set up driver and database
+ * @constructor
+ */
+ public function __construct($container)
+ {
+ $this->setContainer($container);
+ $client = new \MongoDB\Driver\Manager(Config::get(['storage', 'Mongo', 'host_uri']));
+ $this->databaseName = Config::get(['storage', 'Mongo', 'db_name']);
+ $this->client = $client;
+ }
+
+ /**
+ * Inserts the document into the specified collection.
+ *
+ * @param string$collection Name of the collection to insert to
+ * @param API\Document\DocumentInterface $document The document to be inserted
+ * @return DocumentResult The result of this query
+ */
+ public function insertOne($collection, $document)
+ {
+ $bulk = new \MongoDB\Driver\BulkWrite();
+ if ($document instanceof DocumentInterface) {
+ $document = $document->toArray();
+ }
+ $bulk->insert($document);
+
+ $result = $this->getClient()->executeBulkWrite($this->databaseName . '.' . $collection, $bulk);
+ return $result;
+ }
+
+ /**
+ * Inserts the document into the specified collection.
+ *
+ * @param string $collection Name of the collection to insert to
+ * @param array $documents Collection of API\Document\DocumentInterface documents to be inserted
+ * @return DocumentResult The result of this query
+ */
+ public function insertMultiple($collection, $documents)
+ {
+ $bulk = new \MongoDB\Driver\BulkWrite();
+ foreach ($documents as $document) {
+ if ($document instanceof DocumentInterface) {
+ $document = $document->toArray();
+ }
+ $bulk->insert($document);
+ }
+
+ $result = $this->getClient()->executeBulkWrite($this->databaseName . '.' . $collection, $bulk);
+ return $result;
+ }
+
+ /**
+ * Update a document or create a new document when no document matches the query criteria.
+ *
+ * @param string $collection Name of the collection to insert to
+ * @param array $filter The filter that determines which documents to update
+ * @param object|array $newDocument The modified document to be written
+ * @return DocumentResult The result of this query
+ */
+ public function upsert($collection, $filter, $newDocument)
+ {
+ return $this->update($collection, $filter, $newDocument, true);
+ }
+
+ /**
+ * Upserts documents matching the filter.
+ *
+ * @param string $collection Name of collection
+ * @param array $filter The filter that determines which documents to update
+ * @param object|array $newDocument The modified document to be written
+ * @param bool $upsert
+ * @return DocumentResult The result of this query
+ */
+ public function update($collection, $filter, $newDocument, $upsert = false)
+ {
+ if ($filter instanceof Mongo\ExpressionInterface) {
+ $filter = $filter->toArray();
+ }
+
+ $bulk = new \MongoDB\Driver\BulkWrite();
+ if ($newDocument instanceof DocumentInterface) {
+ $newDocument = $newDocument->toArray();
+ }
+ $updateOptions = ['upsert' => $upsert];
+
+ $bulk->update($filter, $newDocument, $updateOptions);
+
+ $result = $this->getClient()->executeBulkWrite($this->databaseName . '.' . $collection, $bulk);
+ return $result;
+ }
+
+ /**
+ * Deletes documents.
+ *
+ * @param string $collection Name of collection
+ * @param array $filter The filter that matches documents the need to be deleted
+ * @return DeletionResult Result of deletion
+ */
+ public function delete($collection, $filter)
+ {
+ if ($filter instanceof Mongo\ExpressionInterface) {
+ $filter = $filter->toArray();
+ }
+ $bulk = new \MongoDB\Driver\BulkWrite();
+ $bulk->delete($filter);
+
+ $result = $this->getClient()->executeBulkWrite($this->databaseName . '.' . $collection, $bulk);
+ return $result;
+ }
+
+ /**
+ * Fetches documents.
+ *
+ * @param string $collection Name of collection
+ * @param array|Expression $filter The filter to fetch the documents by
+ * @param array $options
+ * @return DocumentResult Result of fetch
+ */
+ public function find($collection, $filter = [], $options = [])
+ {
+ if ($filter instanceof Mongo\ExpressionInterface) {
+ $filter = $filter->toArray();
+ }
+
+ $query = new \MongoDB\Driver\Query($filter, $options);
+ $cursor = $this->getClient()->executeQuery($this->databaseName . '.' . $collection, $query);
+
+ return $cursor;
+ }
+
+ /**
+ * Fetches distinct documents.
+ *
+ * @param string $collection Name of collection
+ * @param array|Expression $filter The filter to fetch the documents by
+ * @param array $options
+ * @return DocumentResult Result of fetch
+ */
+ public function distinct($collection, $field, $filter = [], $options = [])
+ {
+ // query doesn't accept empty array: [MongoDB\Driver\Exception\RuntimeException] "query" had the wrong type. Expected object or null, found array
+ $command = new Command([
+ 'distinct' => $collection,
+ 'key' => $field,
+ 'query' => (empty($filter)) ? null : $filter,
+ 'options' => $options,
+ ]);
+
+ $cursor = $this->getClient()->executeCommand($this->databaseName, $command);
+
+ return $cursor;
+ }
+
+ /**
+ * Fetches documents.
+ *
+ * @param string $collection Name of collection
+ * @param array|Expression $filter The filter to fetch the documents by
+ * @param array $options
+ * @return DocumentResult Result of fetch
+ */
+ public function findOne($collection, $filter, $options = [])
+ {
+ if ($filter instanceof Mongo\ExpressionInterface) {
+ $filter = $filter->toArray();
+ }
+ $options = ['limit' => 1] + $options;
+ $query = new \MongoDB\Driver\Query($filter, $options);
+ $cursor = $this->getClient()->executeQuery($this->databaseName . '.' . $collection, $query);
+ $document = current($cursor->toArray());
+ return ($document === false) ? null : $document;
+ }
+
+ /**
+ * Count
+ *
+ * @param string $collection Name of collection
+ * @param array|Expression $filter The filter to fetch the documents by
+ * @param array $options
+ * @return int
+ */
+ public function count($collection, $filter = [], $options = [])
+ {
+ if ($filter instanceof Mongo\ExpressionInterface) {
+ $filter = $filter->toArray();
+ }
+ $command = ['count' => $collection];
+ if (!empty($filter)) {
+ $command['query'] = $filter;
+ }
+
+ foreach (['hint', 'limit', 'maxTimeMS', 'skip'] as $option) {
+ if (isset($options[$option])) {
+ $command[$option] = $options[$option];
+ }
+ }
+
+ $command = new Command($command);
+
+ $cursor = $this->getClient()->executeCommand($this->databaseName, $command);
+ $result = current($cursor->toArray());
+
+ // Older server versions may return a float
+ if (!isset($result->n) || ! (is_integer($result->n) || is_float($result->n))) {
+ throw new AdapterException('Count command did not return a numeric "n" value');
+ }
+ return (integer) $result->n;
+ }
+
+ /**
+ * Create a Mongo Expression
+ *
+ * @return Mongo\Expression
+ */
+ public function createExpression()
+ {
+ $expression = new Mongo\Expression();
+ return $expression;
+ }
+
+ ////
+ // Admin Tools
+ ////
+
+ /**
+ * Checks if a Mongo Command exists.
+ * This operation is costly and shoudl only be called during admin/install routines
+ * @param string $search Mongo command name
+ *
+ * @return bool
+ */
+ public function supportsCommand($search)
+ {
+ $cursor = $this->executeCommand(['listCommands' => 1]);
+ $result = $cursor->toArray()[0];
+ $cmds = $result->commands;
+
+ return isset($cmds->{$search});
+ }
+
+ /**
+ * Check if minimum requred Mongo version is installed
+ * @param string $available Use for unit tests only
+ *
+ * @return array with storage version info if compatible
+ * @throws AdapterException if installed version is lower than required
+ */
+ public function verifyDatabaseVersion($available = null)
+ {
+ $available = ($available) ? $available : $this->getDatabaseversion();
+ $required = self::REQUIRED_DB_VERSION;
+
+ $msg = [
+ 'installed' => $available,
+ 'required' => $required,
+ ];
+
+ if (version_compare($available, $required) < 0) {
+ throw new AdapterException('Database version mismatch: ' . json_encode($msg) . "\n".'Please upgrade your Database!');
+ }
+
+ return $msg;
+ }
+
+ /**
+ * Perform a Mongo command.
+ * @see http://php.net/manual/en/class.mongodb-driver-command.php
+ * @see http://php.net/manual/en/mongodb-driver-manager.executecommand.php
+ *
+ * @param array|object $args command document
+ * @return Cursor MondoDb cursor
+ */
+ public function executeCommand($args)
+ {
+ $command = new Command($args);
+ $cursor = $this->getClient()->executeCommand($this->databaseName, $command);
+ return $cursor;
+ }
+
+ /**
+ * Create indexes for a collection
+ * @param string $collection collection name, (will be autocreated)
+ * @param array|object $indexes indexes to be created
+ * @return Cursor|null MondoDb cursor
+ */
+ public function createIndexes($collection, $indexes)
+ {
+ if (empty($indexes)) {
+ return null;
+ }
+
+ $args = [
+ "createIndexes" => $collection,
+ "indexes" => $indexes,
+ ];
+
+ $cursor = $this->executeCommand($args);
+ return $cursor;
+ }
+
+ // TODO 0.11.x: Maybe remove these methods and call them in their respective Service classes
+ // These helpers only add extra complexity and lookups
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getStatementStorage()
+ {
+ $storage = new Mongo\Statement($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAttachmentStorage()
+ {
+ $storage = new Mongo\Attachment($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getUserStorage()
+ {
+ $storage = new Mongo\User($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getLogStorage()
+ {
+ $storage = new Mongo\Log($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getActivityStorage()
+ {
+ $storage = new Mongo\Activity($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getActivityStateStorage()
+ {
+ $storage = new Mongo\ActivityState($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getActivityProfileStorage()
+ {
+ $storage = new Mongo\ActivityProfile($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAgentProfileStorage()
+ {
+ $storage = new Mongo\AgentProfile($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getBasicAuthStorage()
+ {
+ $storage = new Mongo\BasicAuth($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOAuthStorage()
+ {
+ $storage = new Mongo\OAuth($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOAuthClientsStorage()
+ {
+ $storage = new Mongo\OAuthClients($this->getContainer());
+ return $storage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getDatabaseversion()
+ {
+ $result = $this->executeCommand(['buildinfo' => 1]);
+
+ // another option would tbe 'buildinfo.versionArray' property
+ // however, it is not clear how widely this is supported
+ return $result->toArray()[0]->version;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function testConnection($uri)
+ {
+ $client = new \MongoDB\Driver\Manager($uri);
+ $command = new \MongoDB\Driver\Command(['buildinfo' => 1]);
+ $result = $client->executeCommand('admin', $command);
+
+ if ($result) {
+ $result = $result->toArray()[0];
+ } else {
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Gets the value of client.
+ *
+ * @return mixed
+ */
+ public function getClient()
+ {
+ return $this->client;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Activity.php b/src/xAPI/Storage/Adapter/Mongo/Activity.php
new file mode 100644
index 00000000..4362b8be
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Activity.php
@@ -0,0 +1,101 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\ActivityInterface;
+
+use API\Controller;
+use API\Storage\Provider;
+
+use API\Storage\AdapterException;
+
+class Activity extends Provider implements ActivityInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'activities';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ [
+ 'name' => 'id.unique',
+ 'key' => [
+ 'id' => 1
+ ],
+ 'unique' => true,
+ ]
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function fetchById($id)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('id', $id);
+
+ if ($storage->count(self::COLLECTION_NAME, $expression) === 0) {
+ throw new AdapterException('Activity does not exist.', Controller::STATUS_NOT_FOUND);
+ }
+
+ $document = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ return $document;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/ActivityProfile.php b/src/xAPI/Storage/Adapter/Mongo/ActivityProfile.php
new file mode 100644
index 00000000..000bc3c4
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/ActivityProfile.php
@@ -0,0 +1,300 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\ActivityProfileInterface;
+
+use API\Controller;
+use API\Util;
+use API\Storage\Query\DocumentResult;
+use API\Storage\Provider;
+
+use API\Storage\AdapterException;
+
+// TODO 0.11.x remove header dependency from this layer into parser and submit abstract args from there, like an array of options: put($data, $profileId, $agentIfi, array $options (contentType, if match))
+
+class ActivityProfile extends Provider implements ActivityProfileInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'activityProfiles';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ //profileId is not unique as per spec, only combination of profileId and activityId
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFiltered($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ // Single activity state
+ if (isset($parameters['profileId'])) {
+ $expression->where('profileId', $parameters['profileId']);
+ $expression->where('activityId', $parameters['activityId']);
+
+ $cursorCount = $storage->count(self::COLLECTION_NAME, $expression);
+ $this->validateCursorCountValid($cursorCount);
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(true);
+ $documentResult->setRemainingCount(1);
+ $documentResult->setTotalCount(1);
+
+ return $documentResult;
+ }
+
+ $expression->where('activityId', $parameters['activityId']);
+
+ if (isset($parameters['since'])) {
+ $since = Util\Date::dateStringToMongoDate($parameters['since']);
+ $expression->whereGreaterOrEqual('mongoTimestamp', $since);
+ }
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(false);
+
+ return $documentResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function post($parameters, $profileObject)
+ {
+ return $this->put($parameters, $profileObject);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function put($parameters, $profileObject)
+ {
+ // TODO 0.11.x: optimise (upsert)
+ // rawPayload input is a stream...read it
+ $profileObject = (string)$profileObject;
+
+ $storage = $this->getContainer()->get('storage');
+
+ // Set up the body to be saved
+ $activityProfileDocument = new \API\Document\Generic();
+
+ // Check for existing state - then merge if applicable
+ $expression = $storage->createExpression();
+ $expression->where('profileId', $parameters['profileId']);
+ $expression->where('activityId', $parameters['activityId']);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+ if ($result) {
+ $result = new \API\Document\Generic($result);
+ }
+
+ $ifMatchHeader = isset($parameters['headers']['if-match']) ? $parameters['headers']['if-match'] : null;
+ $ifNoneMatchHeader = isset($parameters['headers']['if-none-match']) ? $parameters['headers']['if-none-match'] : null;
+ $this->validateMatchHeaders($ifMatchHeader, $ifNoneMatchHeader, $result);
+
+ $contentType = $parameters['headers']['content-type'];
+ if ($contentType === null || empty($contentType)) {
+ $contentType = 'text/plain';
+ } else {
+ $contentType = $contentType[0];
+ }
+
+ // ID exists, try to merge body if applicable
+ if ($result) {
+ $this->validateDocumentType($result, $contentType);
+
+ // This validation is still in arrays, which is why we decode to them
+ // Since a validation engine change is upcoming, this is currently left as is
+ $decodedExisting = json_decode($result->getContent(), true);
+ $this->validateJsonDecodeErrors();
+
+ $decodedPosted = json_decode($profileObject, true);
+ $this->validateJsonDecodeErrors();
+
+ $profileObject = json_encode(array_merge($decodedExisting, $decodedPosted));
+ $activityProfileDocument = $result;
+ }
+
+ $activityProfileDocument->setContent($profileObject);
+ // Dates
+ $currentDate = Util\Date::dateTimeExact();
+ $activityProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
+ $activityProfileDocument->setActivityId($parameters['activityId']);
+ $activityProfileDocument->setProfileId($parameters['profileId']);
+ $activityProfileDocument->setContentType($contentType);
+ $activityProfileDocument->setHash(sha1($profileObject));
+
+ $storage->update(self::COLLECTION_NAME, $expression, $activityProfileDocument, true);
+
+ return $activityProfileDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function delete($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('profileId', $parameters['profileId']);
+ $expression->where('activityId', $parameters['activityId']);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ $cursorCount = $storage->count(self::COLLECTION_NAME, $expression);
+
+ $this->validateCursorCountValid($cursorCount);
+
+ $ifMatchHeader = isset($parameters['headers']['if-match']) ? $parameters['headers']['if-match'] : null;
+ $ifNoneMatchHeader = isset($parameters['headers']['if-none-match']) ? $parameters['headers']['if-none-match'] : null;
+
+ $this->validateMatchHeaders($ifMatchHeader, $ifNoneMatchHeader, $result);
+
+ $deletionResult = $storage->delete(self::COLLECTION_NAME, $expression);
+
+ return $deletionResult;
+ }
+
+ private function validateMatchHeaders($ifMatch, $ifNoneMatch, $result)
+ {
+ // If-Match first
+ $ifMatch = isset($ifMatch[0]) ? $ifMatch[0] : false;
+ if ($ifMatch && $result && ($this->trimHeader($ifMatch) !== $result->getHash())) {
+ throw new AdapterException('If-Match header doesn\'t match the current ETag.', Controller::STATUS_PRECONDITION_FAILED);
+ }
+
+ // Then If-None-Match
+ $ifNoneMatch = isset($ifNoneMatch[0]) ? $ifNoneMatch[0] : false;
+ if ($ifNoneMatch) {
+ if ($this->trimHeader($ifNoneMatch) === '*' && $result) {
+ throw new AdapterException('If-None-Match header is *, but a resource already exists.', Controller::STATUS_PRECONDITION_FAILED);
+ } elseif ($result && $this->trimHeader($ifNoneMatch) === $result->getHash()) {
+ throw new AdapterException('If-None-Match header matches the current ETag.', Controller::STATUS_PRECONDITION_FAILED);
+ }
+ }
+ }
+
+ private function validateMatchHeaderExists($ifMatch, $ifNoneMatch, $result)
+ {
+ // Check If-Match and If-None-Match here
+ if (!$ifMatch && !$ifNoneMatch && $result) {
+ throw new AdapterException('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Controller::STATUS_CONFLICT);
+ }
+ }
+
+ private function validateDocumentType($document, $contentType)
+ {
+ if ($document->getContentType() !== 'application/json') {
+ throw new AdapterException('Original document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ if ($contentType !== 'application/json') {
+ throw new AdapterException('Posted document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateTargetDocumentType($document)
+ {
+ if ($document->getContentType() !== 'application/json') {
+ throw new AdapterException('Original document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateSourceDocumentType($documentType)
+ {
+ if ($documentType !== 'application/json') {
+ throw new AdapterException('Posted document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateCursorCountValid($cursorCount)
+ {
+ if ($cursorCount === 0) {
+ throw new AdapterException('Agent profile does not exist.', Controller::STATUS_NOT_FOUND);
+ }
+ }
+
+ private function validateJsonDecodeErrors()
+ {
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new AdapterException('Invalid JSON in existing document. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * Trims quotes from the header.
+ *
+ * @param string $headerString Header
+ *
+ * @return string Trimmed header
+ */
+ private function trimHeader($headerString)
+ {
+ return trim($headerString, '"');
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/ActivityState.php b/src/xAPI/Storage/Adapter/Mongo/ActivityState.php
new file mode 100644
index 00000000..b2e90c7c
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/ActivityState.php
@@ -0,0 +1,299 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\ActivityStateInterface;
+
+use API\Util;
+use API\Controller;
+use API\Storage\Provider;
+use API\Storage\Query\DocumentResult;
+
+use API\Storage\AdapterException;
+
+// TODO 0.11.x remove header dependency from this layer into parser and submit abstract args from there, like an array of options: put($data, $profileId, $agentIfi, array $options (contentType, if match))
+
+class ActivityState extends Provider implements ActivityStateInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'activityStates';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ //stateId is not unique as per spec, only combination of stateId and activityId
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFiltered($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $agent = $parameters->get('agent');
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing ifi', Controller::STATUS_BAD_REQUEST);
+ }
+
+ // Single activity state
+ if (isset($parameters['stateId'])) {
+ $expression->where('stateId', $parameters->get('stateId'));
+ $expression->where('activityId', $parameters->get('activityId'));
+
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ if (isset($parameters['registration'])) {
+ $expression->where('registration', $parameters->get('registration'));
+ }
+
+ $cursorCount = $storage->count(self::COLLECTION_NAME, $expression);
+
+ $this->validateCursorCountValid($cursorCount);
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(true);
+ $documentResult->setRemainingCount(1);
+ $documentResult->setTotalCount(1);
+
+ return $documentResult;
+ }
+
+ $expression->where('activityId', $parameters->get('activityId'));
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ if ($parameters->has('registration')) {
+ $expression->where('registration', $parameters->get('registration'));
+ }
+
+ if ($parameters->has('since')) {
+ $since = Util\Date::dateStringToMongoDate($parameters->get('since'));
+ $expression->whereGreaterOrEqual('mongoTimestamp', $since);
+ }
+
+ // Fetch
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(false);
+
+ return $documentResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function post($parameters, $stateObject)
+ {
+ return $this->put($parameters, $stateObject);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ */
+ public function put($parameters, $stateObject)
+ {
+ // TODO 0.11.x: optimise (upsert)
+ // rawPayload input is a stream...read it
+ $stateObject = (string)$stateObject;
+
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ // Set up the body to be saved
+ $activityStateDocument = new \API\Document\Generic();
+
+ // Check for existing state - then merge if applicable
+ $expression = $storage->createExpression();
+
+ // Check for existing state - then merge if applicable
+ $expression->where('stateId', $parameters->get('stateId'));
+ $expression->where('activityId', $parameters->get('activityId'));
+
+ $agent = $parameters->get('agent');
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing ifi', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ if ($parameters->has('registration')) {
+ $expression->where('registration', $parameters->get('registration'));
+ }
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+ if ($result) {
+ $result = new \API\Document\Generic($result);
+ }
+
+ // ID exists, merge body
+ $contentType = $parameters['headers']['content-type'];
+ if ($contentType === null || empty($contentType)) {
+ $contentType = 'text/plain';
+ } else {
+ $contentType = $contentType[0];
+ }
+
+ // ID exists, try to merge body if applicable
+ if ($result) {
+ $this->validateDocumentType($result, $contentType);
+
+ $decodedExisting = json_decode($result->getContent(), true);
+ $this->validateJsonDecodeErrors();
+
+ $decodedPosted = json_decode($stateObject, true);
+ $this->validateJsonDecodeErrors();
+
+ $stateObject = json_encode(array_merge($decodedExisting, $decodedPosted));
+ $activityStateDocument = $result;
+ }
+
+ $activityStateDocument->setContent($stateObject);
+ // Dates
+ $currentDate = Util\Date::dateTimeExact();
+ $activityStateDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
+
+ $activityStateDocument->setActivityId($parameters->get('activityId'));
+ $activityStateDocument->setAgent($agent);
+ if ($parameters->has('registration')) {
+ $activityStateDocument->setRegistration($parameters->get('registration'));
+ }
+ $activityStateDocument->setStateId($parameters->get('stateId'));
+ $activityStateDocument->setContentType($contentType);
+ $activityStateDocument->setHash(sha1($stateObject));
+
+ $storage->upsert(self::COLLECTION_NAME, $expression, $activityStateDocument);
+
+ return $activityStateDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function delete($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ if ($parameters->has('stateId')) {
+ $expression->where('stateId', $parameters->get('stateId'));
+ }
+
+ $expression->where('activityId', $parameters->get('activityId'));
+
+ $agent = $parameters->get('agent');
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing IFI', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ if ($parameters->has('registration')) {
+ $expression->where('registration', $parameters->get('registration'));
+ }
+
+ $deletionResult = $storage->delete(self::COLLECTION_NAME, $expression);
+ return $deletionResult;
+ }
+
+ private function validateCursorCountValid($cursorCount)
+ {
+ if ($cursorCount === 0) {
+ throw new AdapterException('Activity state does not exist.', Controller::STATUS_NOT_FOUND);
+ }
+ }
+
+ private function validateJsonDecodeErrors()
+ {
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new AdapterException('Invalid JSON in existing document. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateDocumentType($document, $contentType)
+ {
+ if ($document->getContentType() !== 'application/json') {
+ throw new AdapterException('Original document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ if ($contentType !== 'application/json') {
+ throw new AdapterException('Posted document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/AgentProfile.php b/src/xAPI/Storage/Adapter/Mongo/AgentProfile.php
new file mode 100644
index 00000000..be1aad3b
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/AgentProfile.php
@@ -0,0 +1,326 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\AgentProfileInterface;
+
+use API\Util;
+use API\Controller;
+use API\Storage\Provider;
+use API\Storage\Query\DocumentResult;
+
+use API\Storage\AdapterException;
+
+// TODO 0.11.x remove header dependency from this layer into parser and submit abstract args from there, like an array of options: put($data, $profileId, $agentIfi, array $options (contentType, if match))
+
+class AgentProfile extends Provider implements AgentProfileInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'agentProfiles';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ //profileId is not unique as per spec, only combination of profileId and agent
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getFiltered($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ // Single activity profile
+ if ($parameters->has('profileId')) {
+ $expression->where('profileId', $parameters['profileId']);
+ $agent = $parameters['agent'];
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing ifi', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ $cursorCount = $storage->count(self::COLLECTION_NAME, $expression);
+ $this->validateCursorCountValid($cursorCount);
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(true);
+ $documentResult->setRemainingCount(1);
+ $documentResult->setTotalCount(1);
+
+ return $documentResult;
+ }
+
+ $agent = $parameters['agent'];
+ $agent = json_decode($agent);
+ $expression->where('agent', $agent);
+
+ if ($parameters->has('since')) {
+ $since = Util\Date::dateStringToMongoDate($parameters['since']);
+ $expression->whereGreaterOrEqual('mongoTimestamp', $since);
+ }
+
+ // Fetch
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $documentResult = new DocumentResult();
+ $documentResult->setCursor($cursor);
+ $documentResult->setIsSingle(false);
+
+ return $documentResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function post($parameters, $profileObject)
+ {
+ return $this->put($parameters, $profileObject);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function put($parameters, $profileObject)
+ {
+ // TODO 0.11.x: optimise (upsert)
+ // rawPayload input is a stream...read it
+ $profileObject = (string)$profileObject;
+ $agent = $parameters['agent'];
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing ifi', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $storage = $this->getContainer()->get('storage');
+
+ // Set up the body to be saved
+ $agentProfileDocument = new \API\Document\Generic();
+
+ // Check for existing state - then merge if applicable
+ $expression = $storage->createExpression();
+ $expression->where('profileId', $parameters['profileId']);
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+ if ($result) {
+ $result = new \API\Document\Generic($result);
+ }
+
+ $ifMatchHeader = isset($parameters['headers']['if-match']) ? $parameters['headers']['if-match'] : false;
+ $ifNoneMatchHeader = isset($parameters['headers']['if-none-match']) ? $parameters['headers']['if-none-match'] : false;
+ $this->validateMatchHeaderExists($ifMatchHeader, $ifNoneMatchHeader, $result);
+ $this->validateMatchHeaders($ifMatchHeader, $ifNoneMatchHeader, $result);
+
+ // ID exists, merge body
+ $contentType = $parameters['headers']['content-type'];
+ if ($contentType === null || empty($contentType)) {
+ $contentType = 'text/plain';
+ } else {
+ $contentType = $contentType[0];
+ }
+
+ // ID exists, try to merge body if applicable
+ if ($result) {
+ $this->validateSourceDocumentType($contentType);
+ $this->validateTargetDocumentType($result);
+
+ $decodedExisting = json_decode($result->getContent(), true);
+ $this->validateJsonDecodeErrors();
+
+ $decodedPosted = json_decode($profileObject, true);
+ $this->validateJsonDecodeErrors();
+
+ $profileObject = json_encode(array_merge($decodedExisting, $decodedPosted));
+ $agentProfileDocument = $result;
+ }
+
+ $agentProfileDocument->setContent($profileObject);
+ // Dates
+ $currentDate = Util\Date::dateTimeExact();
+ $agentProfileDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
+ $agentProfileDocument->setAgent($agent);
+ $agentProfileDocument->setProfileId($parameters['profileId']);
+ $agentProfileDocument->setContentType($contentType);
+ $agentProfileDocument->setHash(sha1($profileObject));
+
+ $storage->upsert(self::COLLECTION_NAME, $expression, $agentProfileDocument);
+
+ return $agentProfileDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function delete($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('profileId', $parameters['profileId']);
+ $agent = $parameters['agent'];
+ $agent = json_decode($agent, true);
+
+ // TODO 0.11.x move to validator layer, add to jsonschema
+ // from https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#agentprofres
+ // The "agent" parameter is an Agent Object and not a Group. Learning Record Providers wishing to store data against an Identified Group can use the Identified Group's identifier within an Agent Object.
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Invalid `agent` parameter: missing ifi', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $expression->where('agent.'.$uniqueIdentifier, $agent[$uniqueIdentifier]);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ if ($result) {
+ $result = new \API\Document\Generic($result);
+ } else {
+ throw new AdapterException('Profile does not exist!.', Controller::STATUS_NOT_FOUND);
+ }
+
+ $ifMatchHeader = isset($parameters['headers']['if-match']) ? $parameters['headers']['if-match'] : false;
+ $ifNoneMatchHeader = isset($parameters['headers']['if-none-match']) ? $parameters['headers']['if-none-match'] : false;
+ $this->validateMatchHeaderExists($ifMatchHeader, $ifNoneMatchHeader, $result);
+ $this->validateMatchHeaders($ifMatchHeader, $ifNoneMatchHeader, $result);
+
+ $deletionResult = $storage->delete(self::COLLECTION_NAME, $expression);
+ }
+
+ private function validateMatchHeaders($ifMatch, $ifNoneMatch, $result)
+ {
+ // If-Match first
+ $ifMatch = (is_array($ifMatch) && isset($ifMatch[0])) ? $ifMatch[0] : $ifMatch;
+ if ($ifMatch && $result && ($this->trimHeader($ifMatch) !== $result->getHash())) {
+ throw new AdapterException('If-Match header doesn\'t match the current) ETag.', Controller::STATUS_PRECONDITION_FAILED);
+ }
+
+ // Then If-None-Match
+ $ifNoneMatch = (is_array($ifNoneMatch) && isset($ifNoneMatch[0])) ? $ifNoneMatch[0] : $ifNoneMatch;
+ if ($ifNoneMatch) {
+ if ($this->trimHeader($ifNoneMatch) === '*' && $result) {
+ throw new AdapterException('If-None-Match header is *, but a resource already exists.', Controller::STATUS_PRECONDITION_FAILED);
+ } elseif ($result && $this->trimHeader($ifNoneMatch) === $result->getHash()) {
+ throw new AdapterException('If-None-Match header matches the current ETag.', Controller::STATUS_PRECONDITION_FAILED);
+ }
+ }
+ }
+
+ private function validateMatchHeaderExists($ifMatch, $ifNoneMatch, $result)
+ {
+ // Check If-Match and If-None-Match here
+ if (!$ifMatch && !$ifNoneMatch && $result) {
+ throw new AdapterException('There was a conflict. Check the current state of the resource and set the "If-Match" header with the current ETag to resolve the conflict.', Controller::STATUS_CONFLICT);
+ }
+ }
+
+ private function validateTargetDocumentType($document)
+ {
+ if ($document->getContentType() !== 'application/json') {
+ throw new AdapterException('Original document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateSourceDocumentType($documentType)
+ {
+ if ($documentType !== 'application/json') {
+ throw new AdapterException('Posted document is not JSON. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateCursorCountValid($cursorCount)
+ {
+ if ($cursorCount === 0) {
+ throw new AdapterException('Agent profile does not exist.', Controller::STATUS_NOT_FOUND);
+ }
+ }
+
+ private function validateJsonDecodeErrors()
+ {
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new AdapterException('Invalid JSON in existing document. Cannot merge!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * Trims quotes from the header.
+ *
+ * @param string $headerString Header
+ *
+ * @return string Trimmed header
+ */
+ private function trimHeader($headerString)
+ {
+ return trim($headerString, '"');
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Attachment.php b/src/xAPI/Storage/Adapter/Mongo/Attachment.php
new file mode 100644
index 00000000..24466ed3
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Attachment.php
@@ -0,0 +1,107 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\AttachmentInterface;
+
+use API\Util;
+use API\Storage\Provider;
+
+class Attachment extends Provider implements AttachmentInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'attachments';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ // TODO 0.11.x: Enable attachment indexing - planned for
+ // This will require checksum matching to check for existing attachments
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function store($sha2, $contentType, $timestamp = null)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ $attachmentDocument = new \API\Document\Generic();
+ $attachmentDocument->setSha2($sha2);
+ $attachmentDocument->setContentType($contentType);
+ if (null === $timestamp) {
+ $timestamp = new \DateTime();
+ $timestamp = Util\Date::dateTimeToMongoDate($timestamp);
+ }
+ $attachmentDocument->setTimestamp($timestamp);
+ $storage->insertOne(self::COLLECTION_NAME, $attachmentDocument);
+
+ return $attachmentDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function fetchMetadataBySha2($sha2)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ $expression = $storage->createExpression();
+ $expression->where('sha2', $sha2);
+ $document = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ return $document;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/BasicAuth.php b/src/xAPI/Storage/Adapter/Mongo/BasicAuth.php
new file mode 100644
index 00000000..3c980a7f
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/BasicAuth.php
@@ -0,0 +1,212 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\BasicAuthInterface;
+
+use API\Controller;
+use API\Storage\Provider;
+
+use API\Storage\AdapterException;
+use API\Util;
+
+class BasicAuth extends Provider implements BasicAuthInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'basicTokens';
+
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ [
+ 'name' => 'key.unique',
+ 'key' => [
+ 'key' => 1
+ ],
+ 'unique' => true,
+ ]
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function storeToken($name, $description, $expiresAt, $user, $permissions, $key = null, $secret = null)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $accessTokenDocument = new \API\Document\AccessToken();
+ $accessTokenDocument->setName($name);
+ $accessTokenDocument->setDescription($description);
+ $accessTokenDocument->setUserId($user->_id);
+
+ $accessTokenDocument->setPermissions($permissions);
+
+ if (isset($expiresAt)) {
+ $expiresDate = new \DateTime();
+ $expiresDate->setTimestamp($expiresAt);
+ $accessTokenDocument->setExpiresAt(\API\Util\Date::dateTimeToMongoDate($expiresDate));
+ }
+
+ if (null !== $key) {
+ $accessTokenDocument->setKey($key);
+ } else {
+ // Generate token
+ $accessTokenDocument->setKey(\API\Util\OAuth::generateToken());
+ }
+
+ if (null !== $secret) {
+ $accessTokenDocument->setSecret($secret);
+ } else {
+ // Generate token
+ $accessTokenDocument->setSecret(\API\Util\OAuth::generateToken());
+ }
+
+ $currentDate = new \DateTime();
+ $accessTokenDocument->setCreatedAt(\API\Util\Date::dateTimeToMongoDate($currentDate));
+
+ $storage->insertOne(self::COLLECTION_NAME, $accessTokenDocument);
+
+ return $accessTokenDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getToken($key, $secret)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('key', $key);
+ $expression->where('secret', $secret);
+ $accessTokenDocument = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ $this->validateAccessTokenNotEmpty($accessTokenDocument);
+
+ $this->validateExpiration($accessTokenDocument);
+
+ $accessTokenDocumentTransformed = new \API\Document\BasicToken($accessTokenDocument);
+
+ // Fetch user for this token - this is done here intentionally for performance reasons
+ // We could call $storage->getUserStorage() as well, but it'd be slower
+ $accessTokenUser = $storage->findOne(User::COLLECTION_NAME, ['_id' => $accessTokenDocument->userId]);
+
+ $accessTokenDocumentTransformed->setUser($accessTokenUser);
+
+ // Set the host - needed for generation of access token authority
+ $host = $this->getContainer()->get('url')->getBaseUrl();
+ $accessTokenDocumentTransformed->setHost($host);
+
+ return $accessTokenDocumentTransformed;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function deleteToken($key)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('key', $key);
+
+ $deletionResult = $storage->delete(self::COLLECTION_NAME, $expression);
+
+ return $deletionResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function expireToken($key)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('key', $key);
+ $updateResult = $storage->update(self::COLLECTION_NAME, $expression, ['$set' => ['expired' => true]]);
+
+ return $updateResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTokens()
+ {
+ $storage = $this->getContainer()->get('storage');
+ $cursor = $storage->find(self::COLLECTION_NAME);
+
+ return $cursor;
+ }
+
+ private function validateExpiration($accessTokenDocument)
+ {
+ if (isset($accessTokenDocument->expiresAt) && $accessTokenDocument->expiresAt !== null) {
+ if (Util\Date::mongoDateToTimestamp($accessTokenDocument->expiresAt) <= time()) {
+ throw new AdapterException('Expired token.', Controller::STATUS_FORBIDDEN);
+ }
+ }
+ }
+
+ private function validateAccessTokenNotEmpty($accessToken)
+ {
+ if ($accessToken === null) {
+ throw new AdapterException('Invalid credentials.', Controller::STATUS_FORBIDDEN);
+ }
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Expression.php b/src/xAPI/Storage/Adapter/Mongo/Expression.php
new file mode 100644
index 00000000..a41f789d
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Expression.php
@@ -0,0 +1,703 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from sokil/php-mongo.
+ * License information is available at https://github.com/sokil/php-mongo/blob/master/LICENSE
+ *
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use FieldType;
+use API\Storage\AdapterException;
+
+class Expression implements ExpressionInterface
+{
+ /**
+ * @var array $expression
+ */
+ protected $expression = [];
+
+ /**
+ * @constructor
+ * @param array $array
+ */
+ public function __construct($array = [])
+ {
+ $this->expression = $array;
+ }
+
+ ////
+ // self::$expression management
+ ////
+
+ /**
+ * Create self::$expression from array
+ * @param array $array
+ *
+ * @return void
+ */
+ public function fromArray($array)
+ {
+ $this->expression = $array;
+ }
+
+ /**
+ * Create new instance of expression
+ * @return self
+ */
+ public function expression()
+ {
+ return new self;
+ }
+
+
+ /**
+ * Helper method for fetching self::$expresssion
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->expression;
+ }
+
+ /**
+ * Merge expression into self::$expression
+ * @param Expression $expression
+ *
+ * @return self
+ */
+ public function merge(Expression $expression)
+ {
+ $this->expression = array_merge_recursive($this->expression, $expression->toArray());
+ return $this;
+ }
+
+ /**
+ * Transform expression in different formats to canonical array form
+ * @param mixed $mixed
+ *
+ * @return array
+ * @throws AdapterException
+ */
+ public static function convertToArray($mixed)
+ {
+ // Get expression from callable
+ if (is_callable($mixed)) {
+ $callable = $mixed;
+ $mixed = new self();
+ call_user_func($callable, $mixed);
+ }
+
+ // Get expression array
+ if ($mixed instanceof self) {
+ $mixed = $mixed->toArray();
+ } elseif (!is_array($mixed)) {
+ throw new AdapterException('Mixed must be instance of \Expression');
+ }
+
+ return $mixed;
+ }
+
+ ////
+ // where query
+ ////
+
+ /**
+ * Performs where search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function where($field, $value)
+ {
+ if (!isset($this->expression[$field]) || !is_array($value) || !is_array($this->expression[$field])) {
+ $this->expression[$field] = $value;
+ } else {
+ $this->expression[$field] = array_merge_recursive($this->expression[$field], $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Performs whereEmpty search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereEmpty($field)
+ {
+ return $this->where('$or', [
+ [$field => null],
+ [$field => ''],
+ [$field => []],
+ [$field => ['$exists' => false]]
+ ]);
+ }
+
+ /**
+ * Performs whereNotEmpty search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereNotEmpty($field)
+ {
+ return $this->where('$nor', [
+ [$field => null],
+ [$field => ''],
+ [$field => []],
+ [$field => ['$exists' => false]]
+ ]);
+ }
+
+ /**
+ * Performs whereGreater value search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function whereGreater($field, $value)
+ {
+ return $this->where($field, ['$gt' => $value]);
+ }
+
+ /**
+ * Performs whereGreaterOrEqual value search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function whereGreaterOrEqual($field, $value)
+ {
+ return $this->where($field, ['$gte' => $value]);
+ }
+
+ /**
+ * Performs whereLess value search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function whereLess($field, $value)
+ {
+ return $this->where($field, ['$lt' => $value]);
+ }
+
+ /**
+ * Performs whereLessOrEqual value search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function whereLessOrEqual($field, $value)
+ {
+ return $this->where($field, ['$lte' => $value]);
+ }
+
+ /**
+ * Performs whereNotEqual value search.
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return self
+ */
+ public function whereNotEqual($field, $value)
+ {
+ return $this->where($field, ['$ne' => $value]);
+ }
+
+ /**
+ * Selects the documents where the value of a
+ * field equals any value in the specified array.
+ *
+ * @param string $field
+ * @param array $values
+ * @return \Expression
+ */
+ public function whereIn($field, array $values)
+ {
+ return $this->where($field, ['$in' => $values]);
+ }
+
+ /**
+ * Performs whereNotIn values search.
+ * @param string $field
+ * @param array $values
+ *
+ * @return self
+ */
+ public function whereNotIn($field, array $values)
+ {
+ return $this->where($field, ['$nin' => $values]);
+ }
+
+ /**
+ * Performs whereExists search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereExists($field)
+ {
+ return $this->where($field, ['$exists' => true]);
+ }
+
+ /**
+ * Performs whereNotExists search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereNotExists($field)
+ {
+ return $this->where($field, ['$exists' => false]);
+ }
+
+ /**
+ * Performs whereHasType search.
+ * @see API\Storage\Adapter\Mongo\FieldType
+ *
+ * @param string $field
+ * @param int $type
+ *
+ * @return self
+ */
+ public function whereHasType($field, $type)
+ {
+ return $this->where($field, ['$type' => (int) $type]);
+ }
+
+ ////
+ // where query: field types
+ ////
+
+ /**
+ * Performs whereDouble search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereDouble($field)
+ {
+ return $this->whereHasType($field, FieldType::DOUBLE);
+ }
+
+ /**
+ * Performs whereString search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereString($field)
+ {
+ return $this->whereHasType($field, FieldType::STRING);
+ }
+
+ /**
+ * Performs whereObject search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereObject($field)
+ {
+ return $this->whereHasType($field, FieldType::OBJECT);
+ }
+
+ /**
+ * Performs whereBoolean search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereBoolean($field)
+ {
+ return $this->whereHasType($field, FieldType::BOOLEAN);
+ }
+
+ /**
+ * Performs whereArray search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereArray($field)
+ {
+ return $this->whereJsCondition('Array.isArray(this.' . $field . ')');
+ }
+
+ /**
+ * Performs whereArrayOfArrays search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereArrayOfArrays($field)
+ {
+ return $this->whereHasType($field, FieldType::ARRAY_TYPE);
+ }
+
+ /**
+ * Performs whereObjectId search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereObjectId($field)
+ {
+ return $this->whereHasType($field, FieldType::OBJECT_ID);
+ }
+
+ /**
+ * Performs whereDate search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereDate($field)
+ {
+ return $this->whereHasType($field, FieldType::DATE);
+ }
+
+ /**
+ * Performs whereNul search.
+ * @param string $field
+ *
+ * @return self
+ */
+ public function whereNull($field)
+ {
+ return $this->whereHasType($field, FieldType::NULL);
+ }
+
+ /**
+ * Performs whereJsCondition search. Find documents with Mongos $where
+ * @param Expression $condition
+ *
+ * @return self
+ */
+ public function whereJsCondition($condition)
+ {
+ return $this->where('$where', $condition);
+ }
+
+ ////
+ // where query: like
+ ////
+
+ /**
+ * Performs whereLike search. Find documents where the value matches a regex pattern
+ * @param string $field point-delimited field name
+ * @param string $regex
+ * @param bool $caseInsensitive
+ *
+ * @return self
+ */
+ public function whereLike($field, $regex, $caseInsensitive = true)
+ {
+ // Regex
+ $expression = [
+ '$regex' => $regex,
+ ];
+
+ // Options
+ $options = '';
+
+ if ($caseInsensitive) {
+ $options .= 'i';
+ }
+
+ $expression['$options'] = $options;
+
+ // Query
+ return $this->where($field, $expression);
+ }
+
+ ////
+ // where query: value matches
+ ////
+
+ /**
+ * Performs whereAll search.Find documents where the value of a field is an array
+ * that contains all the specified elements.
+ * This is equivalent of logical AND.
+ * @see http://docs.mongodb.org/manual/reference/operator/query/all/
+ *
+ * @param string $field point-delimited field name
+ * @param array $values
+ *
+ * @return self
+ */
+ public function whereAll($field, array $values)
+ {
+ return $this->where($field, ['$all' => $values]);
+ }
+
+ /**
+ * Performs whereNoneOf search. Find documents where the value of a field is an array
+ * that contains none of the specified elements.
+ * This is equivalent of logical AND.
+ * @see http://docs.mongodb.org/manual/reference/operator/query/all/
+ *
+ * @param string $field point-delimited field name
+ * @param array $values
+ *
+ * @return self
+ */
+ public function whereNoneOf($field, array $values)
+ {
+ return $this->where($field, [
+ '$not' => [
+ '$all' => $values
+ ],
+ ]);
+ }
+
+ /**
+ * Performs whereAny search. Find documents where the value of a field is an array
+ * that contains any of the specified elements.
+ * This is equivalent of logical AND.
+ * @param string $field point-delimited field name
+ * @param array $values
+ *
+ * @return self
+ */
+ public function whereAny($field, array $values)
+ {
+ return $this->whereIn($field, $values);
+ }
+
+ /**
+ * Performs whereElemMatch search.Matches documents in a collection that contain an array field with at
+ * least one element that matches all the specified query criteria.
+ * @param string $field point-delimited field name
+ * @param Expression|callable|array $expression
+ *
+ * @return self
+ * @throws AdapterException
+ */
+ public function whereElemMatch($field, $expression)
+ {
+ if (is_callable($expression)) {
+ $expression = call_user_func($expression, $this->expression());
+ }
+
+ if ($expression instanceof Expression) {
+ $expression = $expression->toArray();
+ } elseif (!is_array($expression)) {
+ throw new AdapterException('Wrong expression passed');
+ }
+
+ return $this->where($field, ['$elemMatch' => $expression]);
+ }
+
+ /**
+ * Performs whereElemNotMatch search. Matches documents in a collection that contain an array field with elements
+ * that do not matches all the specified query criteria.
+ * @param type $field
+ * @param Expression|callable|array $expression
+ *
+ * @return self
+ */
+ public function whereElemNotMatch($field, $expression)
+ {
+ return $this->whereNot($this->expression()->whereElemMatch($field, $expression));
+ }
+
+ /**
+ * Performs whereArraySize search. Selects documents if the array field is a specified size.
+ * @param string $field
+ * @param integer $length
+ *
+ * @return self
+ */
+ public function whereArraySize($field, $length)
+ {
+ return $this->where($field, ['$size' => (int) $length]);
+ }
+
+ ////
+ // where query: logical
+ ////
+
+ /**
+ * Performs whereOr search. Selects the documents that satisfy at least one of the expressions
+ * @param array|Expression $expressions Array of Expression instances
+ *
+ * @return self
+ */
+ public function whereOr($expressions = null)
+ {
+ if ($expressions instanceof Expression) {
+ $expressions = func_get_args();
+ }
+
+ return $this->where('$or', array_map(function (Expression $expression) {
+ return $expression->toArray();
+ }, $expressions));
+ }
+
+ /**
+ * Performs whereAnd search. Select the documents that satisfy all the expressions in the array
+ * @param array|Expression $expressions Array of Expression instances
+ *
+ * @return self
+ */
+ public function whereAnd($expressions = null)
+ {
+ if ($expressions instanceof Expression) {
+ $expressions = func_get_args();
+ }
+
+ return $this->where('$and', array_map(function (Expression $expression) {
+ return $expression->toArray();
+ }, $expressions));
+ }
+
+ /**
+ * Performs whereNor search. Selects the documents that fail all the query expressions in the array
+ * @param array[Expression]$expressions Array of Expression instances
+ *
+ * @return self
+ */
+ public function whereNor($expressions = null)
+ {
+ if ($expressions instanceof Expression) {
+ $expressions = func_get_args();
+ }
+
+ return $this->where('$nor', array_map(function (Expression $expression) {
+ return $expression->toArray();
+ }, $expressions));
+ }
+
+ /**
+ * Performs where search
+ * @param Expression $expression
+ *
+ * @return self
+ */
+ public function whereNot(Expression $expression)
+ {
+ foreach ($expression->toArray() as $field => $value) {
+ // $not acceptable only for operators-expressions
+ if (is_array($value) && is_string(key($value))) {
+ $this->where($field, ['$not' => $value]);
+ }
+ // for single values use $ne
+ else {
+ $this->whereNotEqual($field, $value);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Select documents where the value of a field divided by a divisor has the specified remainder (i.e. perform a modulo operation to select documents)
+ * @param string $field
+ * @param int $divisor
+ * @param int $remainder
+ *
+ * @return self
+ */
+ public function whereMod($field, $divisor, $remainder)
+ {
+ $this->where($field, array(
+ '$mod' => [(int) $divisor, (int) $remainder],
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Perform fulltext search
+ *
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/
+ * @link https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/
+ *
+ * If a collection contains documents or embedded documents that are in different languages,
+ * include a field named language in the documents or embedded documents and specify as its value the language
+ * for that document or embedded document.
+ *
+ * The specified language in the document overrides the default language for the text index.
+ * The specified language in an embedded document override the language specified in an enclosing document or
+ * the default language for the index.
+ *
+ * Case Insensitivity:
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/#text-operator-case-sensitivity
+ *
+ * Diacritic Insensitivity:
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/#text-operator-diacritic-sensitivity
+ *
+ * @param $search A string of terms that MongoDB parses and uses to query the text index. MongoDB performs a
+ * logical OR search of the terms unless specified as a phrase.
+ * @param $language Optional. The language that determines the list of stop words for the search and the
+ * rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index.
+ * If you specify a language value of "none", then the text search uses simple tokenization
+ * with no list of stop words and no stemming.
+ * @param bool|false $caseSensitive Allowed from v.3.2 A boolean flag to enable or disable case
+ * sensitive search. Defaults to false; i.e. the search defers to the case insensitivity of the text index.
+ * @param bool|false $diacriticSensitive Allowed from v.3.2 A boolean flag to enable or disable diacritic
+ * sensitive search against version 3 text indexes. Defaults to false; i.e. the search defers to the diacritic
+ * insensitivity of the text index. Text searches against earlier versions of the text index are inherently
+ * diacritic sensitive and cannot be diacritic insensitive. As such, the $diacriticSensitive option has no
+ * effect with earlier versions of the text index.
+ *
+ * @return self
+ */
+ public function whereText(
+ $search,
+ $language = null,
+ $caseSensitive = null,
+ $diacriticSensitive = null
+ ) {
+ $this->expression['$text'] = [
+ '$search' => $search,
+ ];
+
+ if ($language) {
+ $this->expression['$text']['$language'] = $language;
+ }
+
+ // Version 3.2 feature
+ if ($caseSensitive) {
+ $this->expression['$text']['$caseSensitive'] = (bool) $caseSensitive;
+ }
+
+ // Version 3.2 feature
+ if ($diacriticSensitive) {
+ $this->expression['$text']['$diacriticSensitive'] = (bool) $diacriticSensitive;
+ }
+
+ return $this;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/ExpressionInterface.php b/src/xAPI/Storage/Adapter/Mongo/ExpressionInterface.php
new file mode 100644
index 00000000..317b932a
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/ExpressionInterface.php
@@ -0,0 +1,254 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from sokil/php-mongo.
+ * License information is available at https://github.com/sokil/php-mongo/blob/master/LICENSE
+ *
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+interface ExpressionInterface
+{
+ /**
+ * Create new instance of expression
+ * @return \Expression
+ */
+ public function expression();
+
+ /**
+ * Return a expression
+ * @return \Cursor|\Expression
+ */
+ public function where($field, $value);
+
+ public function whereEmpty($field);
+
+ public function whereNotEmpty($field);
+
+ public function whereGreater($field, $value);
+
+ public function whereGreaterOrEqual($field, $value);
+
+ public function whereLess($field, $value);
+
+ public function whereLessOrEqual($field, $value);
+
+ public function whereNotEqual($field, $value);
+
+ /**
+ * Selects the documents where the value of a
+ * field equals any value in the specified array.
+ *
+ * @param string $field
+ * @param array $values
+ * @return \Expression
+ */
+ public function whereIn($field, array $values);
+
+ public function whereNotIn($field, array $values);
+
+ public function whereExists($field);
+
+ public function whereNotExists($field);
+
+ public function whereHasType($field, $type);
+
+ public function whereDouble($field);
+
+ public function whereString($field);
+
+ public function whereObject($field);
+
+ public function whereBoolean($field);
+
+ public function whereArray($field);
+
+ public function whereArrayOfArrays($field);
+
+ public function whereObjectId($field);
+
+ public function whereDate($field);
+
+ public function whereNull($field);
+
+ public function whereJsCondition($condition);
+
+ public function whereLike($field, $regex, $caseInsensitive = true);
+
+ /**
+ * Find documents where the value of a field is an array
+ * that contains all the specified elements.
+ * This is equivalent of logical AND.
+ *
+ * @link http://docs.mongodb.org/manual/reference/operator/query/all/
+ *
+ * @param string $field point-delimited field name
+ * @param array $values
+ * @return \Expression
+ */
+ public function whereAll($field, array $values);
+
+ /**
+ * Find documents where the value of a field is an array
+ * that contains none of the specified elements.
+ * This is equivalent of logical AND.
+ *
+ * @link http://docs.mongodb.org/manual/reference/operator/query/all/
+ *
+ * @param string $field point-delimited field name
+ * @param array $values
+ * @return \Expression
+ */
+ public function whereNoneOf($field, array $values);
+
+ /**
+ * Find documents where the value of a field is an array
+ * that contains any of the specified elements.
+ * This is equivalent of logical AND.
+ *
+ * @param string $field point-delimited field name
+ * @param array $values
+ * @return \Expression
+ */
+ public function whereAny($field, array $values);
+
+ /**
+ * Matches documents in a collection that contain an array field with at
+ * least one element that matches all the specified query criteria.
+ *
+ * @param string $field point-delimited field name
+ * @param \Expression|callable|array $expression
+ * @return \Expression
+ */
+ public function whereElemMatch($field, $expression);
+
+ /**
+ * Matches documents in a collection that contain an array field with elements
+ * that do not matches all the specified query criteria.
+ *
+ * @param type $field
+ * @param \Expression|callable|array $expression
+ * @return \Expression
+ */
+ public function whereElemNotMatch($field, $expression);
+
+ /**
+ * Selects documents if the array field is a specified size.
+ *
+ * @param string $field
+ * @param integer $length
+ * @return \Expression
+ */
+ public function whereArraySize($field, $length);
+
+ /**
+ * Selects the documents that satisfy at least one of the expressions
+ *
+ * @param array|\Expression $expressions Array of Expression instances or comma delimited expression list
+ * @return \Expression
+ */
+ public function whereOr($expressions = null);
+
+ /**
+ * Select the documents that satisfy all the expressions in the array
+ *
+ * @param array|\Expression $expressions Array of Expression instances or comma delimited expression list
+ * @return \Expression
+ */
+ public function whereAnd($expressions = null);
+
+ /**
+ * Selects the documents that fail all the query expressions in the array
+ *
+ * @param array|\Expression $expressions Array of Expression instances or comma delimited expression list
+ * @return \Expression
+ */
+ public function whereNor($expressions = null);
+
+ public function whereNot(Expression $expression);
+
+ /**
+ * Select documents where the value of a field divided by a divisor has the specified remainder (i.e. perform a modulo operation to select documents)
+ *
+ * @param string $field
+ * @param int $divisor
+ * @param int $remainder
+ */
+ public function whereMod($field, $divisor, $remainder);
+
+ /**
+ * Perform fulltext search
+ *
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/
+ * @link https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/
+ *
+ * If a collection contains documents or embedded documents that are in different languages,
+ * include a field named language in the documents or embedded documents and specify as its value the language
+ * for that document or embedded document.
+ *
+ * The specified language in the document overrides the default language for the text index.
+ * The specified language in an embedded document override the language specified in an enclosing document or
+ * the default language for the index.
+ *
+ * Case Insensitivity:
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/#text-operator-case-sensitivity
+ *
+ * Diacritic Insensitivity:
+ * @link https://docs.mongodb.org/manual/reference/operator/query/text/#text-operator-diacritic-sensitivity
+ *
+ * @param $search A string of terms that MongoDB parses and uses to query the text index. MongoDB performs a
+ * logical OR search of the terms unless specified as a phrase.
+ * @param $language Optional. The language that determines the list of stop words for the search and the
+ * rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index.
+ * If you specify a language value of "none", then the text search uses simple tokenization
+ * with no list of stop words and no stemming.
+ * @param bool|false $caseSensitive Allowed from v.3.2 A boolean flag to enable or disable case
+ * sensitive search. Defaults to false; i.e. the search defers to the case insensitivity of the text index.
+ * @param bool|false $diacriticSensitive Allowed from v.3.2 A boolean flag to enable or disable diacritic
+ * sensitive search against version 3 text indexes. Defaults to false; i.e. the search defers to the diacritic
+ * insensitivity of the text index. Text searches against earlier versions of the text index are inherently
+ * diacritic sensitive and cannot be diacritic insensitive. As such, the $diacriticSensitive option has no
+ * effect with earlier versions of the text index.
+ * @return $this
+ */
+ public function whereText(
+ $search,
+ $language = null,
+ $caseSensitive = null,
+ $diacriticSensitive = null
+ );
+
+ public function toArray();
+
+ public function merge(Expression $expression);
+
+ /**
+ * Transform expression in different formats to canonical array form
+ *
+ * @param mixed $mixed
+ * @return array
+ * @throws \API\Storage\AdapterException
+ */
+ public static function convertToArray($mixed);
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/FieldType.php b/src/xAPI/Storage/Adapter/Mongo/FieldType.php
new file mode 100644
index 00000000..a94e42cd
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/FieldType.php
@@ -0,0 +1,52 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from sokil/php-mongo.
+ * License information is available at https://github.com/sokil/php-mongo/blob/master/LICENSE
+ *
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+class FieldType
+{
+ const DOUBLE = 1;
+ const STRING = 2;
+ const OBJECT = 3;
+ const ARRAY_TYPE = 4;
+ const BINARY_DATA = 5;
+ const UNDEFINED = 6; // deprecated
+ const OBJECT_ID = 7;
+ const BOOLEAN = 8;
+ const DATE = 9;
+ const NULL = 10;
+ const REGULAR_EXPRESSION = 11;
+ const JAVASCRIPT = 13;
+ const SYMBOL = 14;
+ const JAVASCRIPT_WITH_SCOPE = 15;
+ const INT32 = 16;
+ const TIMESTAMP = 17;
+ const INT64 = 18;
+ const MIN_KEY = 255;
+ const MAX_KEY = 127;
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Log.php b/src/xAPI/Storage/Adapter/Mongo/Log.php
new file mode 100644
index 00000000..4e96b8b6
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Log.php
@@ -0,0 +1,90 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\LogInterface;
+
+use API\Util;
+use API\Storage\Provider;
+
+class Log extends Provider implements LogInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'logs';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function logRequest($ip, $method, $endpoint, $timestamp)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $document = new \API\Document\Generic();
+
+ $document->setIp($ip);
+ $document->setMethod($method);
+ $document->setEndpoint($endpoint);
+ $document->setTimestamp(Util\Date::dateTimeToMongoDate($timestamp));
+
+ $storage->insertOne(self::COLLECTION_NAME, $document);
+
+ return $document;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/OAuth.php b/src/xAPI/Storage/Adapter/Mongo/OAuth.php
new file mode 100644
index 00000000..ef85b38e
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/OAuth.php
@@ -0,0 +1,232 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\OAuthInterface;
+
+use API\Controller;
+use API\Util;
+use API\Storage\Provider;
+
+use API\Storage\AdapterException;
+
+class OAuth extends Provider implements OAuthInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'oAuthTokens';
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ [
+ 'name' => 'token.unique',
+ 'key' => [
+ 'token' => 1
+ ],
+ 'unique' => true,
+ ]
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function storeToken($expiresAt, $user, $client, array $permissions = [], $code = null)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ $accessTokenDocument = new \API\Document\Generic();
+
+ $expiresDate = new \DateTime();
+ $expiresDate->setTimestamp($expiresAt);
+ $accessTokenDocument->setExpiresAt(Util\Date::dateTimeToMongoDate($expiresDate));
+ $currentDate = new \DateTime();
+ $accessTokenDocument->setCreatedAt(Util\Date::dateTimeToMongoDate($currentDate));
+
+ $accessTokenDocument->setUserId($user->_id);
+ $accessTokenDocument->setClientId($client->_id);
+
+
+ $accessTokenDocument->setPermissions($permissions);
+ $accessTokenDocument->setToken(Util\OAuth::generateToken());
+ if (null !== $code) {
+ $accessTokenDocument->setCode($code);
+ }
+
+ $storage->insertOne(self::COLLECTION_NAME, $accessTokenDocument);
+
+ return $accessTokenDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getToken($accessToken)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('token', $accessToken);
+ $accessTokenDocument = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ $this->validateAccessTokenNotEmpty($accessTokenDocument);
+
+ $accessTokenDocument = new \API\Document\Generic($accessTokenDocument);
+
+ $this->validateExpiration($accessTokenDocument);
+
+ $accessTokenDocumentTransformed = new \API\Document\OAuthToken($accessTokenDocument);
+
+ // Fetch user for this token - this is done here intentionally for performance reasons
+ // We could call $storage->getUserStorage() as well, but it'd be slower
+ $accessTokenUser = $storage->findOne(User::COLLECTION_NAME, ['_id' => $accessTokenDocument->userId]);
+
+ $accessTokenDocumentTransformed->setUser($accessTokenUser);
+
+ // Set the host - needed for generation of access token authority
+ $host = $this->getContainer()->get('url')->getBaseUrl();
+ $accessTokenDocumentTransformed->setHost($host);
+
+ return $accessTokenDocumentTransformed;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function deleteToken($accessToken)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('token', $accessToken);
+
+ $storage->delete(self::COLLECTION_NAME, $expression);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function expireToken($accessToken)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('token', $accessToken);
+ $storage->update(self::COLLECTION_NAME, $expression, ['expired' => true]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTokenWithOneTimeCode($params)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('code', $params['code']);
+
+ $tokenDocument = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ $this->validateAccessTokenNotEmpty($tokenDocument);
+ $tokenDocument = new \API\Document\AccessToken($tokenDocument);
+
+ // TODO 0.11.x: Add CRUD somewhere method findById to simplify snippets such as this one, or call getClientById on OAuthClients instead...
+ $clientExpression = $storage->createExpression();
+ $clientExpression->where('_id', $tokenDocument->getClientId());
+ $clientDocument = $storage->findOne(OAuthClients::COLLECTION_NAME, $clientExpression);
+
+ $this->validateClientSecret($params, $clientDocument);
+
+ $this->validateRedirectUri($params, $clientDocument);
+
+ // Remove one-time code
+ $tokenDocument->setCode(false);
+
+ $storage->update(self::COLLECTION_NAME, $expression, $tokenDocument);
+
+ return $tokenDocument;
+ }
+
+ private function validateExpiration($token)
+ {
+ if (isset($accessTokenDocument->expiresAt) && $accessTokenDocument->expiresAt !== null) {
+ if ($expiresAt->sec <= time()) {
+ throw new AdapterException('Expired token.', Controller::STATUS_FORBIDDEN);
+ }
+ }
+ }
+
+ private function validateAccessTokenNotEmpty($accessToken)
+ {
+ if ($accessToken === null) {
+ throw new AdapterException('Invalid credentials.', Controller::STATUS_FORBIDDEN);
+ }
+ }
+
+ private function validateClientSecret($params, $clientDocument)
+ {
+ if ($clientDocument->clientId !== $params['client_id'] || $clientDocument->secret !== $params['client_secret']) {
+ throw new AdapterException('Invalid client_id/client_secret combination!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateRedirectUri($params, $clientDocument)
+ {
+ if ($params['redirect_uri'] !== $clientDocument->redirectUri) {
+ throw new AdapterException('Redirect_uri mismatch!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/OAuthClients.php b/src/xAPI/Storage/Adapter/Mongo/OAuthClients.php
new file mode 100644
index 00000000..0cf5d1fe
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/OAuthClients.php
@@ -0,0 +1,126 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\OAuthClientsInterface;
+
+use API\Util;
+use API\Storage\Provider;
+
+class OAuthClients extends Provider implements OAuthClientsInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'oAuthClients';
+
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getClientById($id)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('clientId', $id);
+ $clientDocument = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ return $clientDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getClients()
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ $cursor = $storage->find(self::COLLECTION_NAME);
+ $documentResult = new \API\Storage\Query\DocumentResult();
+ $documentResult->setCursor($cursor);
+
+ return $documentResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function addClient($name, $description, $redirectUri)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ // Set up the Client to be saved
+ $clientDocument = new \API\Document\Generic();
+
+ $clientDocument->setName($name);
+ $clientDocument->setDescription($description);
+ $clientDocument->setRedirectUri($redirectUri);
+
+ $clientId = Util\OAuth::generateToken();
+ $clientDocument->setClientId($clientId);
+
+ $secret = Util\OAuth::generateToken();
+ $clientDocument->setSecret($secret);
+
+ $storage->insertOne(self::COLLECTION_NAME, $clientDocument);
+
+ return $clientDocument;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Schema.php b/src/xAPI/Storage/Adapter/Mongo/Schema.php
new file mode 100644
index 00000000..e0e02821
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Schema.php
@@ -0,0 +1,108 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use MongoDB\Driver\Exception\Exception as MongoException;
+
+use API\BaseTrait;
+use API\Storage\AdapterException;
+use API\Storage\SchemaInterface;
+use API\Storage\Adapter\Mongo as Mongo;
+
+class Schema implements SchemaInterface
+{
+ use BaseTrait;
+
+ /**
+ * Constructor.
+ *
+ * @param PSR-11 Container
+ */
+ public function __construct($container)
+ {
+ $this->setContainer($container);
+ }
+
+ /*
+ * Maps collection names to classnames
+ *
+ * @return void;
+ */
+ public function mapCollections()
+ {
+ return [
+ Activity::COLLECTION_NAME => __NAMESPACE__ . '\\Activity',
+ ActivityProfile::COLLECTION_NAME => __NAMESPACE__ . '\\ActivityProfile',
+ ActivityState::COLLECTION_NAME => __NAMESPACE__ . '\\ActivityState',
+ AgentProfile::COLLECTION_NAME => __NAMESPACE__ . '\\AgentProfile',
+ Attachment::COLLECTION_NAME => __NAMESPACE__ . '\\Attachment',
+ BasicAuth::COLLECTION_NAME => __NAMESPACE__ . '\\BasicAuth',
+ Log::COLLECTION_NAME => __NAMESPACE__ . '\\Log',
+ OAuth::COLLECTION_NAME => __NAMESPACE__ . '\\OAuth',
+ OAuthClients::COLLECTION_NAME => __NAMESPACE__ . '\\OAuthClients',
+ Statement::COLLECTION_NAME => __NAMESPACE__ . '\\Statement',
+ User::COLLECTION_NAME => __NAMESPACE__ . '\\User',
+ ];
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $collections = $this->mapCollections();
+ $container = $this->getContainer();
+
+ // Verify DB compatibility
+ $mongo = new Mongo($container);
+ $mongo->verifyDatabaseVersion();
+
+ foreach ($collections as $collection => $className) {
+ $instance = new $className($container);
+ try {
+ $instance->install();
+ } catch (MongoException $e) {
+ throw new AdapterException('Unable to install collection "' .$collection. '": '.$e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ $indexes = [];
+ $collections = $this->mapCollections();
+ $container = $this->getContainer();
+
+ foreach ($collections as $collection => $className) {
+ $instance = new $className($container);
+ $indexes[$collection] = $instance->getIndexes();
+ }
+
+ return $indexes;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/Statement.php b/src/xAPI/Storage/Adapter/Mongo/Statement.php
new file mode 100644
index 00000000..30281ba5
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/Statement.php
@@ -0,0 +1,672 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\StatementInterface;
+
+use API\Config;
+use API\Controller;
+use API\Util;
+use API\Storage\Provider;
+use API\Storage\Query\StatementResult;
+
+use API\Storage\AdapterException as AdapterException;
+
+use Ramsey\Uuid\Uuid;
+
+class Statement extends Provider implements StatementInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'statements';
+
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ [
+ 'name' => 'statementId.unique',
+ 'key' => [
+ 'statement.id' => 1
+ ],
+ 'unique' => true,
+ ]
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get($parameters)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+ $queryOptions = [];
+
+ $parameters = new Util\Collection($parameters);
+
+ // Single statement
+ if ($parameters->has('statementId')) {
+ $rawStatementId = $parameters->get('statementId');
+ $normalizedStatementId = Util\xAPI::normalizeUuid($rawStatementId);
+ $expression->where('statement.id', $normalizedStatementId);
+ $expression->where('voided', false);
+
+ $this->validateStatementId($parameters['statementId']);
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $cursor = $this->validateCursorNotEmpty($cursor);
+
+ $statementResult = new StatementResult();
+ $statementResult->setCursor($cursor);
+ $statementResult->setRemainingCount(1);
+ $statementResult->setTotalCount(1);
+ $statementResult->setHasMore(false);
+ $statementResult->setSingleStatementRequest(true);
+
+ return $statementResult;
+ }
+
+ if ($parameters->has('voidedStatementId')) {
+ $rawStatementId = $parameters->get('voidedStatementId');
+ $normalizedStatementId = Util\xAPI::normalizeUuid($rawStatementId);
+ $expression->where('statement.id', $normalizedStatementId);
+ $expression->where('voided', true);
+
+ $this->validateStatementId($parameters['voidedStatementId']);
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression);
+
+ $cursor = $this->validateCursorNotEmpty($cursor);
+
+ $statementResult = new StatementResult();
+ $statementResult->setCursor($cursor);
+ $statementResult->setRemainingCount(1);
+ $statementResult->setTotalCount(1);
+ $statementResult->setHasMore(false);
+ $statementResult->setSingleStatementRequest(true);
+
+ return $statementResult;
+ }
+
+ // New StatementResult for non-single statement queries
+ $statementResult = new StatementResult();
+
+ $expression->where('voided', false);
+
+ // Multiple statements
+ if ($parameters->has('agent')) {
+ $agent = $parameters->get('agent');
+ $agent = json_decode($agent, true);
+
+ $uniqueIdentifier = Util\xAPI::extractUniqueIdentifier($agent);
+ $objectType = Util\xAPI::extractIriObjectType($agent);
+
+ // TODO 0.11.x conformance validation: move into validation layer
+ if (null === $uniqueIdentifier && $objectType === 'Group') {
+ throw new AdapterException('No support for querying Anonymous Groups', Controller::STATUS_BAD_REQUEST);
+ }
+ // TODO 0.11.x move into validation layer
+ if (null === $uniqueIdentifier) {
+ throw new AdapterException('Unknown or invalid agent type', Controller::STATUS_BAD_REQUEST);
+ }
+
+ if ($parameters->has('related_agents') && $parameters->get('related_agents') === 'true') {
+ if ($uniqueIdentifier === 'account') {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.authority.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.authority.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.context.team.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.context.team.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.context.instructor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.context.instructor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.object.objectType', 'SubStatement'),
+ $expression->expression()->where('statement.object.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.object.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.authority.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.authority.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.context.team.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.context.team.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.context.instructor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.context.instructor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.object.objectType', 'SubStatement'),
+ $expression->expression()->where('references.object.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.object.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ )
+ )
+ );
+ } else {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('statement.authority.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('statement.context.team.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('statement.context.instructor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.object.objectType', 'SubStatement'),
+ $expression->expression()->where('statement.object.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
+ ),
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.authority.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.context.team.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.context.instructor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.object.objectType', 'SubStatement'),
+ $expression->expression()->where('references.object.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
+ )
+ )
+ );
+ }
+ } else {
+ if ($uniqueIdentifier === 'account') {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ ),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.object.'.$uniqueIdentifier.'.homePage', $agent[$uniqueIdentifier]['homePage']),
+ $expression->expression()->where('references.object.'.$uniqueIdentifier.'.name', $agent[$uniqueIdentifier]['name'])
+ )
+ )
+ );
+ } else {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('statement.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.actor.'.$uniqueIdentifier, $agent[$uniqueIdentifier]),
+ $expression->expression()->where('references.object.'.$uniqueIdentifier, $agent[$uniqueIdentifier])
+ )
+ );
+ }
+ }
+ }
+
+ if ($parameters->has('verb')) {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.verb.id', $parameters->get('verb')),
+ $expression->expression()->where('references.verb.id', $parameters->get('verb'))
+ )
+ );
+ }
+
+ if ($parameters->has('activity')) {
+ // Handle related
+ if ($parameters->has('related_activities') && $parameters->get('related_activities') === 'true') {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.object.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.category.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.grouping.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.other.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->where('statement.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('statement.object.objectType', 'SubStatement'),
+ $expression->expression()->where('statement.object.object', $parameters->get('activity'))
+ ),
+ $expression->expression()->where('references.object.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.category.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.grouping.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.other.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->where('references.context.contextActivities.parent.id', $parameters->get('activity')),
+ $expression->expression()->whereAnd(
+ $expression->expression()->where('references.object.objectType', 'SubStatement'),
+ $expression->expression()->where('references.object.object', $parameters->get('activity'))
+ )
+ )
+ );
+ } else {
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.object.id', $parameters->get('activity')),
+ $expression->expression()->where('references.object.id', $parameters->get('activity'))
+ )
+ );
+ }
+ }
+
+ if ($parameters->has('registration')) {
+ $rawRegistrationId = $parameters->get('registration');
+ $normalizedRegistrationId = Util\xAPI::normalizeUuid($rawRegistrationId);
+ $expression->whereAnd(
+ $expression->expression()->whereOr(
+ $expression->expression()->where('statement.context.registration', $normalizedRegistrationId),
+ $expression->expression()->where('references.context.registration', $normalizedRegistrationId)
+ )
+ );
+ }
+
+ // Date based filters
+ if ($parameters->has('since')) {
+ $since = Util\Date::dateStringToMongoDate($parameters->get('since'));
+ $expression->whereGreaterOrEqual('mongo_timestamp', $since);
+ }
+
+ if ($parameters->has('until')) {
+ $until = Util\Date::dateStringToMongoDate($parameters->get('until'));
+ $expression->whereLessOrEqual('mongo_timestamp', $until);
+ }
+
+ // Count before paginating
+ $statementResult->setTotalCount($storage->count(self::COLLECTION_NAME, $expression, $queryOptions));
+
+ // Handle pagination
+ if ($parameters->has('since_id')) {
+ $id = new \MongoDB\BSON\ObjectID($parameters->get('since_id'));
+ $expression->whereGreater('_id', $id);
+ }
+
+ if ($parameters->has('until_id')) {
+ $id = new \MongoDB\BSON\ObjectID($parameters->get('until_id'));
+ $expression->whereLess('_id', $id);
+ }
+
+ $statementResult->setRequestedFormat(Config::get(['xAPI', 'default_statement_get_format']));
+ if ($parameters->has('format')) {
+ $statementResult->setRequestedFormat($parameters->get('format'));
+ }
+
+ $statementResult->setSortDescending(true);
+ $statementResult->setSortAscending(false);
+ $queryOptions['sort'] = ['_id' => -1];
+ if ($parameters->has('ascending')) {
+ $asc = $parameters->get('ascending');
+ if (strtolower($asc) === 'true' || $asc === '1') {
+ $queryOptions['sort'] = ['_id' => 1];
+ $statementResult->setSortDescending(false);
+ $statementResult->setSortAscending(true);
+ }
+ }
+
+ if ($parameters->has('limit') && $parameters->get('limit') < Config::get(['xAPI', 'statement_get_limit']) && $parameters->get('limit') > 0) {
+ $limit = $parameters->get('limit');
+ } else {
+ $limit = Config::get(['xAPI', 'statement_get_limit']);
+ }
+
+ // Remaining includes the current page!
+ $statementResult->setRemainingCount($storage->count(self::COLLECTION_NAME, $expression, $queryOptions));
+
+ if ($statementResult->getRemainingCount() > $limit) {
+ $statementResult->setHasMore(true);
+ } else {
+ $statementResult->setHasMore(false);
+ }
+
+ $queryOptions['limit'] = (int)$limit;
+
+ // TODO 0.11.x improve following or abstract it into method
+ $auth = $this->getContainer()->get('auth');
+ if ($auth->hasPermission('statements/read/mine') && !$auth->hasPermission('statements/read')) {
+ $expression->where('userId', $this->getAccessToken()->userId);
+ }
+
+ $cursor = $storage->find(self::COLLECTION_NAME, $expression, $queryOptions);
+
+ $statementResult->setCursor($cursor);
+
+ return $statementResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getById($statementId)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+ $expression->where('statement.id', $statementId);
+ $requestedStatement = $storage->findOne('statements', $expression);
+
+ if (null === $requestedStatement) {
+ throw new AdapterException('Requested statement does not exist!', Controller::STATUS_BAD_REQUEST);
+ }
+
+ return $requestedStatement;
+ }
+
+ /**
+ * {@inheritDoc}
+ * // TODO 0.11.x make this rather private and remove from interface
+ */
+ public function transformForInsert($statementObject)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ $attachmentBase = $this->getContainer()->get('url')->getBaseUrl().Config::get(['filesystem', 'exposed_url']);
+
+ if (isset($statementObject->{'id'})) {
+ $expression = $storage->createExpression();
+ $normalizedStatementId = Util\xAPI::normalizeUuid($statementObject->{'id'});
+ $expression->where('statement.id', $normalizedStatementId);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ // ID exists, validate if different or conflict
+ if ($result) {
+ $existingStatement = $result->statement;
+ $this->validateStatementMatches($statementObject, $existingStatement);
+ }
+ }
+
+ $statementDocument = new \API\Document\Statement();
+ // Overwrite authority - unless it's a super token and manual authority is set
+ if (!($this->getAuth()->hasPermission('super') && isset($statementObject->{'authority'})) || !isset($statementObject->{'authority'})) {
+ $statementObject->{'authority'} = $this->getAccessToken()->generateAuthority();
+ }
+ $statementDocument->setStatement($statementObject);
+ // Dates
+ $currentDate = Util\Date::dateTimeExact();
+ $statementDocument->normalizeExistingIds();
+ $statementDocument->setVoided(false);
+ $statementDocument->setStored(Util\Date::dateTimeToISO8601($currentDate));
+ $statementDocument->setMongoTimestamp(Util\Date::dateTimeToMongoDate($currentDate));
+ $statementDocument->setDefaultTimestamp();
+ $statementDocument->fixAttachmentLinks($attachmentBase);
+ $statementDocument->convertExtensionKeysToUnicode();
+ $statementDocument->setDefaultId();
+ $statementDocument->legacyContextActivities();
+ if ($statementDocument->isReferencing()) {
+ // Copy values of referenced statement chain inside current statement for faster query-ing
+ // (space-time tradeoff)
+ $referencedStatementId = $statementDocument->getReferencedStatementId();
+ $referencedStatement = $this->getById($referencedStatementId);
+ $referencedStatement = new \API\Document\Statement($referencedStatement);
+
+ $existingReferences = [];
+ if (null !== $referencedStatement->getReferences()) {
+ $existingReferences = $referencedStatement->getReferences();
+ }
+ $existingReferences[] = $referencedStatement->getStatement();
+ $statementDocument->setReferences($existingReferences);
+ }
+ //$statements[] = $statementDocument->toArray();
+ if ($statementDocument->isVoiding()) {
+ $referencedStatementId = $statementDocument->getReferencedStatementId();
+ $referencedStatement = $this->getById($referencedStatementId);
+ $referencedStatement = new \API\Document\Statement($referencedStatement);
+
+ $this->validateVoidedStatementNotVoiding($referencedStatement);
+ $referencedStatement->setVoided(true);
+ $expression = $storage->createExpression();
+ $expression->where('statement.id', $referencedStatementId);
+
+ $storage->update(self::COLLECTION_NAME, $expression, $referencedStatement);
+ }
+ if ($this->getAuth()->hasPermission('define')) {
+ $activities = $statementDocument->extractActivities();
+ if (count($activities) > 0) {
+ // TODO 0.11.x Possibly optimize this using a bulk update (using executeBulkWrite)
+ // TODO 0.11.x Create upsertMultiple and updateMultiple methods on CRUD layer!
+ foreach ($activities as $activity) {
+ $storage->upsert(Activity::COLLECTION_NAME, ['id' => $activity->id], $activity);
+ }
+ }
+ }
+
+ $statementDocument->setUserId($this->getAccessToken()->getUserId());
+
+ // Add to log (disabled)
+ // $this->getContainer()->get('requestLog')->addRelation('statements', $statementDocument)->save();
+
+ return $statementDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function insertOne($statementObject)
+ {
+ $statementDocument = $this->transformForInsert($statementObject);
+ if (!isset($statementDocument->skipInsert)) {
+ $storage = $this->getContainer()->get('storage');
+ $storage->insertOne(self::COLLECTION_NAME, $statementDocument);
+ } else {
+ unset($statementDocument->skipInsert);
+ }
+ $statementResult = new StatementResult();
+ $statementResult->setCursor([$statementDocument]);
+ $statementResult->setRemainingCount(1);
+ $statementResult->setHasMore(false);
+
+ return $statementResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function insertMultiple($statementObjects)
+ {
+ $statementDocumentsInsert = [];
+ $statementDocumentsView = [];
+ foreach ($statementObjects as $statementObject) {
+ $statementDocument = $this->transformForInsert($statementObject);
+ if (!isset($statementDocument->skipInsert)) {
+ $statementDocumentsInsert[] = $statementDocument;
+ } else {
+ unset($statementDocument->skipInsert);
+ }
+ $statementDocumentsView[] = $statementDocument;
+ }
+
+ $storage = $this->getContainer()->get('storage');
+ $storage->insertMultiple(self::COLLECTION_NAME, $statementDocumentsInsert);
+
+ $statementResult = new StatementResult();
+ $statementResult->setCursor($statementDocumentsView);
+ $statementResult->setRemainingCount(count($statementDocumentsView));
+ $statementResult->setHasMore(false);
+
+ return $statementResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function put($parameters, $statementObject)
+ {
+ $parameters = new Util\Collection($parameters);
+
+ // Check statementId exists
+ if (!$parameters->has('statementId')) {
+ throw new AdapterException('The statementId parameter is missing!', Controller::STATUS_BAD_REQUEST);
+ }
+
+ $this->validateStatementId($parameters['statementId']);
+
+ // Check statementId
+ if (isset($statementObject->id)) {
+ // Check for match
+ $this->validateStatementIdMatch(Util\xAPI::normalizeUuid($statementObject->id), Util\xAPI::normalizeUuid($parameters['statementId']));
+ } else {
+ $statementObject->id = Util\xAPI::normalizeUuid($parameters->get('statementId'));
+ }
+
+ $statementDocument = $this->insertOne($statementObject);
+ $statementResult = new StatementResult();
+ $statementResult->setCursor([$statementDocument]);
+ $statementResult->setRemainingCount(1);
+ $statementResult->setHasMore(false);
+
+ return $statementResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function delete($parameters)
+ {
+ throw AdapterException('Statements cannot be deleted, only voided!', Controller::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ /**
+ * Gets the Auth to validate for permissions.
+ *
+ * @return API\Document\Auth\AbstractToken
+ */
+ private function getAuth()
+ {
+ return $this->getContainer()->get('auth');
+ }
+
+ /**
+ * Gets the Access token to validate for permissions.
+ *
+ * @return API\Document\Auth\AbstractToken
+ */
+ private function getAccessToken()
+ {
+ return $this->getContainer()->get('accessToken');
+ }
+
+ private function validateStatementMatches($incomingStatement, $existingStatement)
+ {
+ // Remove exempted attributes
+ // https://github.com/adlnet/xAPI-Spec/blob/1.0.3/xAPI-Data.md#231-statement-immutability
+ unset($incomingStatement->authority);
+ unset($incomingStatement->stored);
+ unset($incomingStatement->timestamp);
+ unset($incomingStatement->version);
+ unset($existingStatement->authority);
+ unset($existingStatement->stored);
+ unset($existingStatement->timestamp);
+ unset($existingStatement->version);
+ // Mismatch - return 409 Conflict
+ if ($incomingStatement != $existingStatement) {
+ throw new AdapterException('An existing statement already exists with the same ID (' . $existingStatement->id . ') and is different from the one provided.', Controller::STATUS_CONFLICT);
+ }
+ }
+
+ private function validateVoidedStatementNotVoiding($referencedStatement)
+ {
+ if ($referencedStatement->isVoiding()) {
+ throw new AdapterException('Voiding statements cannot be voided.', Controller::STATUS_CONFLICT);
+ }
+ }
+
+ private function validateStatementId($id)
+ {
+ // Check statementId is acutally valid
+ if (!Uuid::isValid($id)) {
+ throw new AdapterException('The provided statement ID is invalid!', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateStatementIdMatch($statementIdOne, $statementIdTwo)
+ {
+ if ($statementIdOne !== $statementIdTwo) {
+ throw new AdapterException('Statement ID query parameter doesn\'t match the given statement property', Controller::STATUS_BAD_REQUEST);
+ }
+ }
+
+ private function validateCursorNotEmpty($cursor)
+ {
+ $cursor = $cursor->toArray();
+ if (empty($cursor)) {
+ throw new AdapterException('Statement does not exist.', Controller::STATUS_NOT_FOUND);
+ }
+ return $cursor;
+ }
+}
diff --git a/src/xAPI/Storage/Adapter/Mongo/User.php b/src/xAPI/Storage/Adapter/Mongo/User.php
new file mode 100644
index 00000000..21bf03bb
--- /dev/null
+++ b/src/xAPI/Storage/Adapter/Mongo/User.php
@@ -0,0 +1,178 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Adapter\Mongo;
+
+use API\Storage\SchemaInterface;
+use API\Storage\Query\UserInterface;
+
+use API\Storage\Provider;
+use API\Controller;
+
+use API\Storage\AdapterException;
+
+class User extends Provider implements UserInterface, SchemaInterface
+{
+ const COLLECTION_NAME = 'users';
+
+
+ /**
+ * @var array $indexes
+ *
+ * @see https://docs.mongodb.com/manual/reference/command/createIndexes/
+ * [
+ * name: ,
+ * key: [
+ * ,
+ * ,
+ * ...
+ * ],
+ * ,
+ * ,
+ * ...
+ * ],
+ */
+ private $indexes = [
+ [
+ 'name' => 'email.unique',
+ 'key' => [
+ 'email' => 1
+ ],
+ 'unique' => true,
+ ]
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function install()
+ {
+ $container = $this->getContainer()->get('storage');
+ $container->executeCommand(['create' => self::COLLECTION_NAME]);
+ $container->createIndexes(self::COLLECTION_NAME, $this->indexes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getIndexes()
+ {
+ return $this->indexes;
+ }
+
+ public function findById($id)
+ {
+ if (is_string($id)) {
+ $id = new \MongoDB\BSON\ObjectID($id);
+ }
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('_id', $id);
+
+ $result = $storage->findOne(self::COLLECTION_NAME, $expression);
+ return $result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function addUser($name, $description, $email, $password, $permissions)
+ {
+ $storage = $this->getContainer()->get('storage');
+
+ // check if email is valid and unique
+ if ($this->hasEmail($email)) {
+ throw new AdapterException('User email exists already.', Controller::STATUS_BAD_REQUEST);
+ }
+
+ // Set up the User to be saved
+ $userDocument = new \API\Document\Generic();
+
+ $userDocument->set('_id', new \MongoDB\BSON\ObjectID());
+ $userDocument->setName($name);
+ $userDocument->setDescription($description);
+ $userDocument->setEmail($email);
+
+ $passwordHash = sha1($password);
+ $userDocument->setPasswordHash($passwordHash);
+
+ $userDocument->setPermissions($permissions);
+
+ $now = new \DateTime();
+ $userDocument->setCreatedAt(\API\Util\Date::dateTimeToMongoDate($now));
+
+ $storage->insertOne(self::COLLECTION_NAME, $userDocument);
+
+ return $userDocument;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function fetchAll()
+ {
+ $storage = $this->getContainer()->get('storage');
+ $cursor = $storage->find(self::COLLECTION_NAME);
+
+ $documentResult = new \API\Storage\Query\DocumentResult();
+ $documentResult->setCursor($cursor);
+
+ return $documentResult;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ // TODO 0.11.x remove this method and references across the code as it is obsolete after indexing user.email as unique
+ public function hasEmail($email)
+ {
+ if (!filter_var($email, \FILTER_VALIDATE_EMAIL)) {
+ return false;
+ }
+
+ $storage = $this->getContainer()->get('storage');
+ $count = $storage->count(self::COLLECTION_NAME, [
+ 'email' => $email,
+ ]);
+
+ return ($count > 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function findByEmailAndPassword($username, $password)
+ {
+ $storage = $this->getContainer()->get('storage');
+ $expression = $storage->createExpression();
+
+ $expression->where('email', $username);
+ $expression->where('passwordHash', sha1($password));
+
+ $document = $storage->findOne(self::COLLECTION_NAME, $expression);
+
+ return $document;
+ }
+}
diff --git a/src/xAPI/Document/AgentProfile.php b/src/xAPI/Storage/AdapterException.php
similarity index 79%
rename from src/xAPI/Document/AgentProfile.php
rename to src/xAPI/Storage/AdapterException.php
index 9e49da54..e74cd8ff 100644
--- a/src/xAPI/Document/AgentProfile.php
+++ b/src/xAPI/Storage/AdapterException.php
@@ -1,9 +1,8 @@
getProfileId();
- }
}
diff --git a/src/xAPI/Storage/AdapterInterface.php b/src/xAPI/Storage/AdapterInterface.php
new file mode 100644
index 00000000..11e85892
--- /dev/null
+++ b/src/xAPI/Storage/AdapterInterface.php
@@ -0,0 +1,181 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage;
+
+interface AdapterInterface
+{
+ /**
+ * Inserts the document into the specified collection.
+ *
+ * @param array $documents array of API\Document\DocumentInterface Documents to be inserted
+ * @param string $collection name Mongo collection name
+ *
+ * @return DocumentResult The result of this query
+ */
+ public function insertMultiple($documents, $collection);
+
+ /**
+ * Inserts the document into the specified collection.
+ *
+ * @param API\Document\DocumentInterface $document The document to be inserted
+ * @param string $collection Name of the collection to insert to
+ *
+ * @return DocumentResult The result of this query
+ */
+ public function insertOne($document, $collection);
+
+ /**
+ * Updates documents matching the filter.
+ *
+ * @param object|array $newDocument The document to be inserted
+ * @param array $filter The query to update the documents
+ * @param string $collection Name of collection
+ *
+ * @return DocumentResult The result of this query
+ */
+ public function update($newDocument, $filter, $collection);
+
+ /**
+ * Deletes documents.
+ *
+ * @param array $query The query that matches documents the need to be deleted
+ * @param string $collection Name of collection
+ * @return DeletionResult Result of deletion
+ */
+ public function delete($query, $collection);
+
+ /**
+ * Fetches documents.
+ *
+ * @param array $query The query to fetch the documents by
+ * @param string $collection Name of collection
+ * @param array $options
+ * @return DocumentResult Result of fetch
+ */
+ public function find($query, $collection, $options = []);
+
+ /**
+ * Fetches documents.
+ *
+ * @param array $query The query to fetch the first document by
+ * @param string $collection Name of collection
+ * @param array $options
+ * @return DocumentResult Result of fetch
+ */
+ public function findOne($query, $collection, $options = []);
+
+ /**
+ * get database version string
+ *
+ * @return string database version (semver)
+ */
+ public function getDatabaseversion();
+
+ /**
+ * Test mongo connection and return buildinfo
+ * @see https://docs.mongodb.com/manual/reference/command/buildInfo/
+ *
+ * @param array|object $args command document
+ * @return object|false buildinfo or false if connection failed
+ */
+ public static function testConnection($uri);
+
+ /**
+ * Get Statement storage
+ *
+ * @return API\Storage\Query\StatementInterface
+ */
+ public function getStatementStorage();
+
+ /**
+ * Get Attachment storage
+ *
+ * @return API\Storage\Query\AttachmentInterface
+ */
+ public function getAttachmentStorage();
+
+ /**
+ * Get User storage
+ *
+ * @return API\Storage\Query\UserInterface
+ */
+ public function getUserStorage();
+
+ /**
+ * Get Log storage
+ *
+ * @return API\Storage\Query\LogInterface
+ */
+ public function getLogStorage();
+
+ /**
+ * Get Activity storage
+ *
+ * @return API\Storage\Query\ActivityInterface
+ */
+ public function getActivityStorage();
+
+ /**
+ * Get ActivityState storage
+ *
+ * @return API\Storage\Query\ActivityStateInterface
+ */
+ public function getActivityStateStorage();
+
+ /**
+ * Get ActivityProfile storage
+ *
+ * @return API\Storage\Query\ActivityProfile
+ */
+ public function getActivityProfileStorage();
+
+ /**
+ * Get AgentProfile storage
+ *
+ * @return API\Storage\Query\AgentProfileInterface
+ */
+ public function getAgentProfileStorage();
+
+ /**
+ * Get BasicAuth storage
+ *
+ * @return API\Storage\Query\BasicAuthInterface
+ */
+ public function getBasicAuthStorage();
+
+ /**
+ * Get OAuth storage
+ *
+ * @return API\Storage\Query\OAuthInterface
+ */
+ public function getOAuthStorage();
+
+ /**
+ * Get OAuthClients storage
+ *
+ * @return API\Storage\Query\OAuthClientsInterface
+ */
+ public function getOAuthClientsStorage();
+}
diff --git a/src/xAPI/Collection/AgentProfiles.php b/src/xAPI/Storage/Provider.php
similarity index 72%
rename from src/xAPI/Collection/AgentProfiles.php
rename to src/xAPI/Storage/Provider.php
index f0e34fa8..078c41b6 100644
--- a/src/xAPI/Collection/AgentProfiles.php
+++ b/src/xAPI/Storage/Provider.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,14 +22,21 @@
* file that was distributed with this source code.
*/
-namespace API\Collection;
+namespace API\Storage;
-use Sokil\Mongo\Collection;
+use API\BaseTrait;
-class AgentProfiles extends Collection
+abstract class Provider
{
- public function getDocumentClassName(array $documentData = null)
+ use BaseTrait;
+
+ /**
+ * Constructor.
+ *
+ * @param \Psr\Container\ContainerInterface $container
+ */
+ public function __construct($container)
{
- return '\\API\\Document\\AgentProfile';
+ $this->setContainer($container);
}
}
diff --git a/src/xAPI/Collection/ActivityProfiles.php b/src/xAPI/Storage/Query/ActivityInterface.php
similarity index 74%
rename from src/xAPI/Collection/ActivityProfiles.php
rename to src/xAPI/Storage/Query/ActivityInterface.php
index 0a5def03..ac8a16a8 100644
--- a/src/xAPI/Collection/ActivityProfiles.php
+++ b/src/xAPI/Storage/Query/ActivityInterface.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,14 +22,15 @@
* file that was distributed with this source code.
*/
-namespace API\Collection;
+namespace API\Storage\Query;
-use Sokil\Mongo\Collection;
-
-class ActivityProfiles extends Collection
+interface ActivityInterface extends QueryInterface
{
- public function getDocumentClassName(array $documentData = null)
- {
- return '\\API\\Document\\ActivityProfile';
- }
+ /**
+ * Find record by Mongo ObjectId
+ * @param string $id
+ *
+ * @return \API\DocumentInterface|null
+ */
+ public function fetchById($id);
}
diff --git a/src/xAPI/Storage/Query/ActivityProfileInterface.php b/src/xAPI/Storage/Query/ActivityProfileInterface.php
new file mode 100644
index 00000000..c4745ec0
--- /dev/null
+++ b/src/xAPI/Storage/Query/ActivityProfileInterface.php
@@ -0,0 +1,65 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface ActivityProfileInterface extends QueryInterface
+{
+ /**
+ * Find single record by Mongo ObjectId
+ *
+ * @param array $parameters map of query params
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getFiltered($parameters);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function post($parameters, $profileObject);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function put($parameters, $profileObject);
+
+ /**
+ * Delete a single record
+ *
+ * @param array $parameters map of query params
+ * @return \API\Storage\Query\API\DeletionResult
+ */
+ public function delete($parameters);
+}
diff --git a/src/xAPI/Storage/Query/ActivityStateInterface.php b/src/xAPI/Storage/Query/ActivityStateInterface.php
new file mode 100644
index 00000000..18326c2d
--- /dev/null
+++ b/src/xAPI/Storage/Query/ActivityStateInterface.php
@@ -0,0 +1,65 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface ActivityStateInterface extends QueryInterface
+{
+ /**
+ * Find records by request params
+ *
+ * @param array $parameters map of query params
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getFiltered($parameters);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function post($parameters, $stateObject);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function put($parameters, $stateObject);
+
+ /**
+ * Delete a single record
+ *
+ * @param array $parameters map of query params
+ * @return \API\Storage\Query\API\DeletionResult
+ */
+ public function delete($parameters);
+}
diff --git a/src/xAPI/Storage/Query/AgentProfileInterface.php b/src/xAPI/Storage/Query/AgentProfileInterface.php
new file mode 100644
index 00000000..430ac089
--- /dev/null
+++ b/src/xAPI/Storage/Query/AgentProfileInterface.php
@@ -0,0 +1,65 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface AgentProfileInterface extends QueryInterface
+{
+ /**
+ * Find single record by Mongo ObjectId
+ *
+ * @param array $parameters map of query params
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getFiltered($parameters);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function post($parameters, $profileObject);
+
+ /**
+ * Upsert a single record
+ *
+ * @param array $parameters map of query params
+ * @param stdClass $profileObject
+ *
+ * @return \API\DocumentInterface
+ */
+ public function put($parameters, $profileObject);
+
+ /**
+ * Delete a single record
+ *
+ * @param array $parameters map of query params
+ * @return \API\Storage\Query\API\DeletionResult
+ */
+ public function delete($parameters);
+}
diff --git a/src/xAPI/Storage/Query/AttachmentInterface.php b/src/xAPI/Storage/Query/AttachmentInterface.php
new file mode 100644
index 00000000..2163c231
--- /dev/null
+++ b/src/xAPI/Storage/Query/AttachmentInterface.php
@@ -0,0 +1,48 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface AttachmentInterface extends QueryInterface
+{
+ /**
+ * Store a record
+ *
+ * @param string $sha2 hash
+ * @param string $contentType
+ * @param int $timestamp
+ *
+ * @return \API\DocumentInterface
+ */
+ public function store($sha2, $contentType, $timestamp = null);
+
+ /**
+ * Fetch a record by sha2 hash
+ *
+ * @param string $sha2 hash
+ *
+ * @return \API\DocumentInterface
+ */
+ public function fetchMetadataBySha2($sha2);
+}
diff --git a/src/xAPI/Storage/Query/BasicAuthInterface.php b/src/xAPI/Storage/Query/BasicAuthInterface.php
new file mode 100644
index 00000000..b72a9332
--- /dev/null
+++ b/src/xAPI/Storage/Query/BasicAuthInterface.php
@@ -0,0 +1,75 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface BasicAuthInterface extends QueryInterface
+{
+
+ /**
+ * Find record by Mongo ObjectId
+ * @param string $name
+ * @param string $description
+ * @param int $expiresAt unix timestamp
+ * @param object $user user storage record
+ * @param array[string] $permissions
+ * @param string $key token key
+ * @param string $secret token secret
+ *
+ * @return \API\DocumentInterface
+ */
+ public function storeToken($name, $description, $expiresAt, $user, $permissions, $key = null, $secret = null);
+
+ /**
+ * Find record by token key and token secret
+ * @param string $key token key
+ * @param string $secret token secret
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getToken($key, $secret);
+
+ /**
+ * Delete record by token key
+ * @param string $key token key
+ *
+ * @return \API\Storage\Query\API\DeletionResult
+ */
+ public function deleteToken($key);
+
+ /**
+ * Expire record by token key
+ * @param string $key token key
+ *
+ * @return \MongoDB\Driver\Cursor
+ */
+ public function expireToken($key);
+
+ /**
+ * Find all records
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getTokens();
+}
diff --git a/src/xAPI/Document/Auth/TokenInterface.php b/src/xAPI/Storage/Query/DeletionResult.php
similarity index 61%
rename from src/xAPI/Document/Auth/TokenInterface.php
rename to src/xAPI/Storage/Query/DeletionResult.php
index 03e3a853..7a1b0aa3 100644
--- a/src/xAPI/Document/Auth/TokenInterface.php
+++ b/src/xAPI/Storage/Query/DeletionResult.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,32 +22,38 @@
* file that was distributed with this source code.
*/
-namespace API\Document\Auth;
+namespace API\Storage\Query;
-interface TokenInterface
+class DeletionResult
{
/**
- * Does the user have a certain permission.
+ * Whether the deletion was successful or not.
*
- * @param string $permissionName Name of the permission
- *
- * @return bool
+ * @var bool
*/
- public function hasPermission($permissionName);
+ protected $success;
/**
- * Throws an exception if the user doesn't possess the given permission.
+ * Gets the Whether the deletion was successful or not.
*
- * @param string $permissionName Name of permission
- *
- * @return void|Exception
+ * @return bool
*/
- public function checkPermission($permissionName);
+ public function getSuccess()
+ {
+ return $this->success;
+ }
/**
- * Is this user valid? I.e. expired token etc.
+ * Sets the Whether the deletion was successful or not.
*
- * @return bool
+ * @param bool $success the success
+ *
+ * @return self
*/
- public function isValid();
+ public function setSuccess($success)
+ {
+ $this->success = $success;
+
+ return $this;
+ }
}
diff --git a/src/xAPI/Storage/Query/DocumentResult.php b/src/xAPI/Storage/Query/DocumentResult.php
new file mode 100644
index 00000000..26d6aa00
--- /dev/null
+++ b/src/xAPI/Storage/Query/DocumentResult.php
@@ -0,0 +1,227 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+use \MongoDB\Driver\Cursor;
+
+class DocumentResult
+{
+ /**
+ * Cursor that contains the result set.
+ *
+ * @var Traversable
+ */
+ protected $cursor;
+
+ /**
+ * Number of total documents that match in entire query.
+ *
+ * @var int
+ */
+ protected $totalCount;
+
+ /**
+ * Number of documents remaining in query where the current skip and limit values are at.
+ *
+ * @var int
+ */
+ protected $remainingCount;
+
+ /**
+ * The number of documents requested in this query (the maximum that can be contained in $cursor).
+ *
+ * @var int
+ */
+ protected $requestedLimit;
+
+ /**
+ * Whether this Result set definitely contains only one element.
+ *
+ * @var bool
+ */
+ protected $isSingle;
+
+ /**
+ * Whether there are more results available, taking into account the number of results being limited.
+ *
+ * @var bool
+ */
+ protected $hasMore;
+
+ /**
+ * Gets the The Cursor with the result set - must implement ArrayAccess or be an array (foreachable).
+ *
+ * @return Cursor
+ */
+ public function getCursor()
+ {
+ return $this->cursor;
+ }
+
+ /**
+ * Sets the The Cursor with the result set - must implement ArrayAccess or be an array (foreachable).
+ *
+ * @param Cursor $cursor
+ *
+ * @return self
+ */
+ public function setCursor($cursor)
+ {
+ $this->cursor = $cursor;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of totalCount.
+ *
+ * @return mixed
+ */
+ public function getTotalCount()
+ {
+ return $this->totalCount;
+ }
+
+ /**
+ * Sets the value of totalCount.
+ *
+ * @param mixed $totalCount the total count
+ *
+ * @return self
+ */
+ public function setTotalCount($totalCount)
+ {
+ $this->totalCount = $totalCount;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of remainingCount.
+ *
+ * @return mixed
+ */
+ public function getRemainingCount()
+ {
+ return $this->remainingCount;
+ }
+
+ /**
+ * Sets the value of remainingCount.
+ *
+ * @param mixed $remainingCount the remaining count
+ *
+ * @return self
+ */
+ public function setRemainingCount($remainingCount)
+ {
+ $this->remainingCount = $remainingCount;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of requestedLimit.
+ *
+ * @return mixed
+ */
+ public function getRequestedLimit()
+ {
+ return $this->requestedLimit;
+ }
+
+ /**
+ * Sets the value of requestedLimit.
+ *
+ * @param mixed $requestedLimit the requested limit
+ *
+ * @return self
+ */
+ public function setRequestedLimit($requestedLimit)
+ {
+ $this->requestedLimit = $requestedLimit;
+
+ return $this;
+ }
+
+ /**
+ * Gets the Whether this Result set definitely contains only one element.
+ *
+ * @return bool
+ */
+ public function getIsSingle()
+ {
+ return $this->isSingle;
+ }
+
+ /**
+ * Sets the Whether this Result set definitely contains only one element.
+ *
+ * @param bool $isSingle the is single
+ *
+ * @return self
+ */
+ public function setIsSingle($isSingle)
+ {
+ $this->isSingle = $isSingle;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of hasMore.
+ *
+ * @return mixed
+ */
+ public function getHasMore()
+ {
+ return $this->hasMore;
+ }
+
+ /**
+ * Sets the value of hasMore.
+ *
+ * @param mixed $hasMore the has more
+ *
+ * @return self
+ */
+ public function setHasMore($hasMore)
+ {
+ $this->hasMore = $hasMore;
+
+ return $this;
+ }
+
+ /**
+ * Unserialize cursor into PHP values.
+ * @see http://php.net/manual/en/mongodb-driver-cursor.toarray.php
+ *
+ * @return object|array returns by default a stdClass if no other typeMap was set for cursor
+ */
+ public function toValues()
+ {
+ return $this->cursor->toArray();
+ }
+}
diff --git a/src/xAPI/Storage/Query/LogInterface.php b/src/xAPI/Storage/Query/LogInterface.php
new file mode 100644
index 00000000..936448fb
--- /dev/null
+++ b/src/xAPI/Storage/Query/LogInterface.php
@@ -0,0 +1,39 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface LogInterface
+{
+ /**
+ * Find record by Mongo ObjectId
+ * @param string $ip ip address
+ * @param string $method request method
+ * @param string $endpoint request endpoint
+ * @param int $timestamp Unix timestamp
+ *
+ * @return \API\DocumentInterface
+ */
+ public function logRequest($ip, $method, $endpoint, $timestamp);
+}
diff --git a/src/xAPI/Resource/V10/About.php b/src/xAPI/Storage/Query/OAuthClientsInterface.php
similarity index 55%
rename from src/xAPI/Resource/V10/About.php
rename to src/xAPI/Storage/Query/OAuthClientsInterface.php
index 47e86c58..3de42c2d 100644
--- a/src/xAPI/Resource/V10/About.php
+++ b/src/xAPI/Storage/Query/OAuthClientsInterface.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,28 +22,33 @@
* file that was distributed with this source code.
*/
-namespace API\Resource\V10;
+namespace API\Storage\Query;
-use API\Resource;
-use API\View\V10\About as AboutView;
-
-class About extends Resource
+interface OAuthClientsInterface extends QueryInterface
{
- // Boilerplate code until this is figured out...
- public function get()
- {
- $versions = $this->getSlim()->config('xAPI')['supported_versions'];
- $view = new AboutView(['versions' => $versions]);
- $view = $view->render();
+ /**
+ * Find record by Mongo ObjectId
+ * @param string $id
+ *
+ * @return \API\DocumentInterface|null
+ */
+ public function getClientById($id);
- Resource::jsonResponse(Resource::STATUS_OK, $view);
- }
+ /**
+ * Find all records
+ *
+ * @return \API\DocumentInterface
+ */
+ public function getClients();
- public function options()
- {
- //Handle options request
- $this->getSlim()->response->headers->set('Allow', 'GET');
- Resource::response(Resource::STATUS_OK);
- }
+ /**
+ * Adds a record
+ * @param string $name
+ * @param string $description
+ * @param string $redirectUri
+ *
+ * @return \API\DocumentInterface
+ */
+ public function addClient($name, $description, $redirectUri);
}
diff --git a/src/xAPI/Storage/Query/OAuthInterface.php b/src/xAPI/Storage/Query/OAuthInterface.php
new file mode 100644
index 00000000..4133f15a
--- /dev/null
+++ b/src/xAPI/Storage/Query/OAuthInterface.php
@@ -0,0 +1,73 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface OAuthInterface extends QueryInterface
+{
+
+ /**
+ * Find record by Mongo ObjectId
+ * @param int $expiresAt unix timestamp
+ * @param object $user user storage record
+ * @param object $client oAuth client storage record
+ * @param array[object] $scopes
+ * @param string|null $code
+ *
+ * @return \API\DocumentInterface
+ */
+ public function storeToken($expiresAt, $user, $client, array $scopes = [], $code = null);
+
+ /**
+ * Find record by token
+ * @param string $accessToken
+ *
+ * @return \API\DocumentInterface|null
+ */
+ public function getToken($accessToken);
+
+ /**
+ * Delete record by token
+ * @param string $accessToken
+ *
+ * @return \API\Storage\Query\API\DeletionResult
+ */
+ public function deleteToken($accessToken);
+
+ /**
+ * Expire record by token
+ * @param string $accessToken
+ *
+ * @return \MongoDB\Driver\Cursor
+ */
+ public function expireToken($accessToken);
+
+ /**
+ * Fetch a token with a time code (?)
+ * @param array map of query params
+ *
+ * @return \MongoDB\Driver\Cursor|null
+ */
+ public function getTokenWithOneTimeCode($params);
+}
diff --git a/src/xAPI/Document/Activity.php b/src/xAPI/Storage/Query/QueryInterface.php
similarity index 86%
rename from src/xAPI/Document/Activity.php
rename to src/xAPI/Storage/Query/QueryInterface.php
index 30abcd0d..9ec0dd38 100644
--- a/src/xAPI/Document/Activity.php
+++ b/src/xAPI/Storage/Query/QueryInterface.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,10 +22,8 @@
* file that was distributed with this source code.
*/
-namespace API\Document;
+namespace API\Storage\Query;
-use Sokil\Mongo\Document;
-
-class Activity extends Document
+interface QueryInterface
{
}
diff --git a/src/xAPI/Storage/Query/StatementInterface.php b/src/xAPI/Storage/Query/StatementInterface.php
new file mode 100644
index 00000000..37dc63fb
--- /dev/null
+++ b/src/xAPI/Storage/Query/StatementInterface.php
@@ -0,0 +1,93 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface StatementInterface extends QueryInterface
+{
+ /**
+ * Get statements using filters.
+ * @param array map of query params
+ *
+ * @return StatementResult
+ */
+ public function get($parameters);
+
+ /**
+ * Find single statement by statementId (not ObjectId!)
+ * @param string $statementId
+ *
+ * @return StatementResult|null
+ */
+ public function getById($statementId);
+
+ /**
+ * Insert single document with params[statementId]
+ * @param array $parameters map of quer yparams
+ *
+ * @return StatementResult
+ * @throws API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function put($parameters, $statementObject);
+
+ /**
+ * Transforms statementObject (parser) into a statementDocument (storage)
+ * @param object $statementObject
+ *
+ * @return \API\DocumentInterface statement document
+ * @throws \API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function transformForInsert($statementObject);
+
+
+ /**
+ * Insert single document
+ * @param object $statementObject
+ *
+ * @return StatementResult|null
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function insertOne($statementObject);
+
+ /**
+ * Insert collection of documents
+ * @param array $statementObjects
+ *
+ * @return StatementResult
+ * @throws API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function insertMultiple($statementObjects);
+
+
+ /**
+ * Ensures that deletion of statements is impossible by throwing always an exception
+ *
+ * @throws API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function delete($parameters);
+}
diff --git a/src/xAPI/Storage/Query/StatementResult.php b/src/xAPI/Storage/Query/StatementResult.php
new file mode 100644
index 00000000..9fa5d5a2
--- /dev/null
+++ b/src/xAPI/Storage/Query/StatementResult.php
@@ -0,0 +1,127 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+class StatementResult extends DocumentResult
+{
+ protected $requestedFormat;
+
+ protected $sortDescending;
+
+ protected $sortAscending;
+
+ protected $singleStatementRequest;
+
+ /**
+ * Gets the value of requestedFormat.
+ * @return mixed
+ */
+ public function getRequestedFormat()
+ {
+ return $this->requestedFormat;
+ }
+
+ /**
+ * Sets the value of requestedFormat.
+ * @param mixed $requestedFormat the requested format
+ *
+ * @return self
+ */
+ public function setRequestedFormat($requestedFormat)
+ {
+ $this->requestedFormat = $requestedFormat;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of sortDescending.
+ *
+ * @return mixed
+ */
+ public function getSortDescending()
+ {
+ return $this->sortDescending;
+ }
+
+ /**
+ * Sets the value of sortDescending.
+ * @param mixed $sortDescending the sort descending
+ *
+ * @return self
+ */
+ public function setSortDescending($sortDescending)
+ {
+ $this->sortDescending = $sortDescending;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of sortAscending.
+ *
+ * @return mixed
+ */
+ public function getSortAscending()
+ {
+ return $this->sortAscending;
+ }
+
+ /**
+ * Sets the value of sortAscending.
+ * @param mixed $sortAscending the sort ascending
+ *
+ * @return self
+ */
+ public function setSortAscending($sortAscending)
+ {
+ $this->sortAscending = $sortAscending;
+
+ return $this;
+ }
+
+ /**
+ * Gets the value of singleStatementRequest.
+ *
+ * @return mixed
+ */
+ public function getSingleStatementRequest()
+ {
+ return $this->singleStatementRequest;
+ }
+
+ /**
+ * Sets the value of singleStatementRequest.
+ * @param mixed $singleStatementRequest the single statement request
+ *
+ * @return self
+ */
+ public function setSingleStatementRequest($singleStatementRequest)
+ {
+ $this->singleStatementRequest = $singleStatementRequest;
+
+ return $this;
+ }
+}
diff --git a/src/xAPI/Storage/Query/UserInterface.php b/src/xAPI/Storage/Query/UserInterface.php
new file mode 100644
index 00000000..2a51dd2d
--- /dev/null
+++ b/src/xAPI/Storage/Query/UserInterface.php
@@ -0,0 +1,75 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Storage\Query;
+
+interface UserInterface extends QueryInterface
+{
+
+ /**
+ * Find record by Mongo ObjectId
+ * @param MongoDB\BSON\ObjectID $id
+ *
+ * @return \API\DocumentInterface|null
+ */
+ public function findById($id);
+
+ /**
+ * Add a user
+ * The only validation we do at this level is ensuring that the email is unique
+ *
+ * @param string $name
+ * @param string $description
+ * @param string $email valid email address
+ * @param string $password
+ * @param array $permissions valid array of permission names
+ *
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function addUser($name, $user, $email, $password, $permissions);
+
+ /**
+ * Find all records
+ * @return \API\DocumentInterface
+ */
+ public function fetchAll();
+
+ /**
+ * Check if collection contains a user with a specified email
+ * @param string $email
+ *
+ * @return bool
+ */
+ public function hasEmail($email);
+
+ /**
+ * Fetch a user record for specified email and password
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return \API\DocumentInterface|null
+ */
+ public function findByEmailAndPassword($username, $password);
+}
diff --git a/src/xAPI/Storage/SchemaInterface.php b/src/xAPI/Storage/SchemaInterface.php
new file mode 100644
index 00000000..498d5896
--- /dev/null
+++ b/src/xAPI/Storage/SchemaInterface.php
@@ -0,0 +1,48 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from sokil/php-mongo.
+ * License information is available at https://github.com/sokil/php-mongo/blob/master/LICENSE
+ *
+ */
+
+namespace API\Storage;
+
+interface SchemaInterface
+{
+ /**
+ * Install hook for collection
+ *
+ * @return void
+ * @throws \API\Storage\AdapterException
+ * @throws \MongoDB\Driver\Exception\Exception
+ */
+ public function install();
+
+ /**
+ * Gets model indexes configuration array
+ *
+ * @return array
+ */
+ public function getIndexes();
+}
diff --git a/src/xAPI/Util/ArrayableInterface.php b/src/xAPI/Util/ArrayableInterface.php
new file mode 100644
index 00000000..732f01cc
--- /dev/null
+++ b/src/xAPI/Util/ArrayableInterface.php
@@ -0,0 +1,8 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from slim.
+ * License information is available at https://github.com/slimphp/Slim/blob/3.x/LICENSE.md
+ *
+ */
+
+namespace API\Util;
+
+use ArrayIterator;
+
+class Collection implements CollectionInterface
+{
+ /**
+ * The source data
+ *
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * Create new collection
+ *
+ * @param array|object $items Pre-populate collection with this key-value array
+ */
+ public function __construct($items = [])
+ {
+ $this->replace((array) $items);
+ }
+
+ /********************************************************************************
+ * Collection interface
+ *******************************************************************************/
+ private function getArray($keys, $default)
+ {
+ $array = $this->data;
+
+ foreach ($keys as $key) {
+ if (isset($array[$key])) {
+ $array = $array[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ return $array;
+ }
+
+ private function hasArray($keys)
+ {
+ $array = $this->data;
+
+ foreach ($keys as $key) {
+ if (isset($array[$key])) {
+ // Loop onwards
+ $array = $array[$key];
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Set collection item
+ *
+ * @param string $key The data key
+ * @param mixed $value The data value
+ */
+ public function set($key, $value)
+ {
+ $this->data[$key] = $value;
+ }
+
+ /**
+ * Get collection item for key
+ *
+ * @param string $key The data key
+ * @param mixed $default The default value to return if data key does not exist
+ *
+ * @return mixed The key's value, or the default value
+ */
+ public function get($key, $default = null)
+ {
+ if (is_array($key)) {
+ return $this->getArray($key, $default);
+ } else {
+ return $this->has($key) ? $this->data[$key] : $default;
+ }
+ }
+
+ /**
+ * Add item to collection, replacing existing items with the same data key
+ *
+ * @param array $items Key-value array of data to append to this collection
+ */
+ public function replace(array $items)
+ {
+ foreach ($items as $key => $value) {
+ $this->set($key, $value);
+ }
+ }
+
+ /**
+ * Get all items in collection
+ *
+ * @return array The collection's source data
+ */
+ public function all()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Get collection keys
+ *
+ * @return array The collection's source data keys
+ */
+ public function keys()
+ {
+ return array_keys($this->data);
+ }
+
+ /**
+ * Does this collection have a given key?
+ *
+ * @param string $key The data key
+ *
+ * @return bool
+ */
+ public function has($key)
+ {
+ if (is_array($key)) {
+ return $this->hasArray($key);
+ } else {
+ return array_key_exists($key, $this->data);
+ }
+ }
+
+ /**
+ * Remove item from collection
+ *
+ * @param string $key The data key
+ */
+ public function remove($key)
+ {
+ unset($this->data[$key]);
+ }
+
+ /**
+ * Remove all items from collection
+ */
+ public function clear()
+ {
+ $this->data = [];
+ }
+
+ /********************************************************************************
+ * Property Overloading
+ *******************************************************************************/
+
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ public function __set($key, $value)
+ {
+ $this->set($key, $value);
+ }
+
+ public function __isset($key)
+ {
+ return $this->has($key);
+ }
+
+ public function __unset($key)
+ {
+ $this->remove($key);
+ }
+
+ /********************************************************************************
+ * Method Overloading
+ *******************************************************************************/
+
+ /**
+ * Handle getters and setters
+ * @param [type] $name [description]
+ * @param [type] $arguments [description]
+ * @return [type] [description]
+ */
+ public function __call($name, $arguments)
+ {
+ // Getter
+ if ('get' === strtolower(substr($name, 0, 3))) {
+ return $this->get(lcfirst(substr($name, 3)));
+ }
+
+ // Setter
+ if ('set' === strtolower(substr($name, 0, 3)) && isset($arguments[0])) {
+ return $this->set(lcfirst(substr($name, 3)), $arguments[0]);
+ }
+ }
+
+ /********************************************************************************
+ * JsonSerializable interface
+ *******************************************************************************/
+
+ public function jsonSerialize()
+ {
+ return $this->data;
+ }
+
+ /********************************************************************************
+ * Arrayable interface
+ *******************************************************************************/
+
+ public function toArray()
+ {
+ return $this->data;
+ }
+
+ /********************************************************************************
+ * ArrayAccess interface
+ *******************************************************************************/
+
+ /**
+ * Does this collection have a given key?
+ *
+ * @param string $key The data key
+ *
+ * @return bool
+ */
+ public function offsetExists($key)
+ {
+ return $this->has($key);
+ }
+
+ /**
+ * Get collection item for key
+ *
+ * @param string $key The data key
+ *
+ * @return mixed The key's value, or the default value
+ */
+ public function offsetGet($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Set collection item
+ *
+ * @param string $key The data key
+ * @param mixed $value The data value
+ */
+ public function offsetSet($key, $value)
+ {
+ $this->set($key, $value);
+ }
+
+ /**
+ * Remove item from collection
+ *
+ * @param string $key The data key
+ */
+ public function offsetUnset($key)
+ {
+ $this->remove($key);
+ }
+
+ /**
+ * Get number of items in collection
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->data);
+ }
+
+ /********************************************************************************
+ * IteratorAggregate interface
+ *******************************************************************************/
+
+ /**
+ * Get collection iterator
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator()
+ {
+ return new ArrayIterator($this->data);
+ }
+}
diff --git a/src/xAPI/Util/CollectionInterface.php b/src/xAPI/Util/CollectionInterface.php
new file mode 100644
index 00000000..d44c0ffc
--- /dev/null
+++ b/src/xAPI/Util/CollectionInterface.php
@@ -0,0 +1,54 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ *
+ * This file was adapted from slim.
+ * License information is available at https://github.com/slimphp/Slim/blob/3.x/LICENSE.md
+ *
+ */
+
+ /*
+ * Slightly altered version of Slim\Interfaces\CollectionInterface (Slim 3)
+ * @author https://github.com/slimphp
+ * @see https://github.com/slimphp/Slim/blob/3.x/Slim/Interfaces/CollectionInterface.php
+ * @see https://www.slimframework.com/
+ */
+
+namespace API\Util;
+
+use API\Util\ArrayableInterface as ArrayableInterface;
+
+interface CollectionInterface extends \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable, ArrayableInterface
+{
+ public function set($key, $value);
+
+ public function get($key, $default = null);
+
+ public function replace(array $items);
+
+ public function all();
+
+ public function has($key);
+
+ public function remove($key);
+
+ public function clear();
+}
diff --git a/src/xAPI/Util/Date.php b/src/xAPI/Util/Date.php
index 93a2eaf3..ad8e8612 100644
--- a/src/xAPI/Util/Date.php
+++ b/src/xAPI/Util/Date.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,13 +25,13 @@
namespace API\Util;
use DateTime;
-use MongoDate;
class Date
{
public static function dateTimeExact()
{
$date = DateTime::createFromFormat('U.u', sprintf('%.f', microtime(true)));
+
return $date;
}
@@ -43,15 +43,32 @@ public static function dateStringToMongoDate($dateString)
return $mongoDate;
}
+ public static function dateTimeToMongoDateLegacy($dateTime)
+ {
+ $seconds = $dateTime->getTimestamp();
+ $microseconds = $dateTime->format('u');
+ $mongoDate = new \MongoDate($seconds, $microseconds);
+
+ return $mongoDate;
+ }
+
public static function dateTimeToMongoDate($dateTime)
{
$seconds = $dateTime->getTimestamp();
$microseconds = $dateTime->format('u');
- $mongoDate = new MongoDate($seconds, $microseconds);
+ $milliSecondTotal = $seconds*1000+(int)($microseconds/1000);
+ $mongoDate = new \MongoDB\BSON\UTCDateTime($milliSecondTotal);
return $mongoDate;
}
+ public static function mongoDateToTimestamp(\MongoDB\BSON\UTCDateTime $mongoDate)
+ {
+ $dateTime = $mongoDate->toDateTime();
+ $timestamp = $dateTime->getTimestamp();
+ return $timestamp;
+ }
+
public static function dateTimeToISO8601($dateTime)
{
$dateTime = $dateTime->format('c');
@@ -80,31 +97,4 @@ public static function dateFromSeconds($seconds)
return $output;
}
-
- /**
- * Attempts to convert a string to a DateTime instance, based on typical (but not all) ISO 8601/RFC 3339 patterns.
- * Adds also millisecond precision.
- * @param string $dateString
- * @param bool $validateStrict is set it checks for precision to at least milliseconds (3 decimal points beyond seconds).
- *
- * @return \DateTime|null
- */
- public static function dateRFC3339($dateString, $validateStrict = false)
- {
- // This is a customized version of the Rfc3339 class in https://github.com/justinrainbow/json-schema.
- // Tests have shown that this is both a very solid and fast solution.
- $pattern = '/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|([+-]\d{2}):?(\d{2}))$/';
- if (!preg_match($pattern, strtoupper($dateString), $matches)) {
- return null;
- }
- $dateAndTime = $matches[1];
- if($validateStrict && !$matches[2]){
- return null;
- }
- $microseconds = $matches[2] ?: '.000';
- $timeZone = 'Z' !== $matches[3] ? $matches[4] . ':' . $matches[5] : '+00:00';
- $dateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $dateAndTime . $microseconds . $timeZone, new \DateTimeZone('UTC'));
- return $dateTime ?: null;
- }
-
}
diff --git a/src/xAPI/Util/Filesystem.php b/src/xAPI/Util/Filesystem.php
index 4e86b9fa..99b4d2ad 100644
--- a/src/xAPI/Util/Filesystem.php
+++ b/src/xAPI/Util/Filesystem.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,10 +24,13 @@
namespace API\Util;
-use League\Flysystem\Filesystem as Flysystem;
+use League\Flysystem\Filesystem as FS;
+use League\Flysystem\Adapter\Local;
use Aws\S3\S3Client;
-use League\Flysystem\Adapter\AwsS3 as S3Adapter;
-use API\Resource;
+use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter;
+
+use API\Controller;
+use API\Config;
// Maybe move this to API/Service and remove ODM dependency on Services. Check out the semantics of this...
class Filesystem
@@ -36,15 +39,32 @@ public static function generateAdapter($config)
{
$typeInUse = $config['in_use'];
if ($typeInUse === 'local') {
- $filesystem = new \League\Flysystem\Filesystem(new \League\Flysystem\Adapter\Local($config['local']['root_dir']));
+ $root = Config::get('publicRoot');
+ $filesystem = new FS(
+ new Local(
+ $root.'/'.$config['local']['root_dir'],
+ \LOCK_EX,
+ Local::DISALLOW_LINKS,
+ [
+ 'file' => [
+ 'public' => 0744,
+ 'private' => 0700,
+ ],
+ 'dir' => [
+ 'public' => 0755,
+ 'private' => 0700,
+ ]
+ ]
+ )
+ );
} elseif ($typeInUse === 's3') {
$client = S3Client::factory(array(
- 'key' => $config['s3']['key'],
+ 'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
));
- $filesystem = new Flysystem(new S3Adapter($client, $config['s3']['bucket_name'], $config['s3']['prefix']));
+ $filesystem = new FS(new S3Adapter($client, $config['s3']['bucket_name'], $config['s3']['prefix']));
} else {
- throw new \Exception('Server error.', Resource::STATUS_INTERNAL_SERVER_ERROR);
+ throw new \Exception('Server error.', Controller::STATUS_INTERNAL_SERVER_ERROR);
}
return $filesystem;
diff --git a/src/xAPI/Util/OAuth.php b/src/xAPI/Util/OAuth.php
index 581b6d74..7d902453 100644
--- a/src/xAPI/Util/OAuth.php
+++ b/src/xAPI/Util/OAuth.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,7 +30,7 @@ public static function generateToken()
{
// Generate a token
$stripped = '';
- $length = 40;
+ $length = 40;
do {
$bytes = openssl_random_pseudo_bytes($length, $strong);
if ($bytes === false || $strong === false) {
diff --git a/src/xAPI/Util/Rememberme/MongoStorage.php b/src/xAPI/Util/Rememberme/MongoStorage.php
deleted file mode 100644
index 3d8ede69..00000000
--- a/src/xAPI/Util/Rememberme/MongoStorage.php
+++ /dev/null
@@ -1,176 +0,0 @@
-.
- *
- * For authorship information, please view the AUTHORS
- * file that was distributed with this source code.
- */
-
-namespace API\Util\Rememberme;
-
-use Birke\Rememberme\Storage\StorageInterface;
-
-/**
- * Sokil/Mongo-Based Storage
- */
-class MongoStorage implements StorageInterface
-{
- /**
- * @var \Sokil\Mongo\Client
- */
- protected $documentManager;
-
- /**
- * @param \Sokil\Mongo\Client $documentManager
- * @param string $suffix
- */
- public function __construct(\Sokil\Mongo\Client $documentManager)
- {
- $this->documentManager = $documentManager;
- }
-
- /**
- * @param mixed $credential
- * @param string $token
- * @param string $persistentToken
- * @return int
- */
- public function findTriplet($credential, $token, $persistentToken)
- {
- // Hash the tokens, because they can contain a salt and can be accessed in the file system
- $persistentToken = sha1($persistentToken);
- $token = sha1($token);
-
- $collection = $this->getDocumentManager()->getCollection('persistentSessions');
- $cursor = $collection->find();
-
- $cursor->where('credential', $credential);
- $cursor->where('persistentToken', $persistentToken);
-
- $document = $cursor->current();
-
- if (null === $document) {
- return self::TRIPLET_NOT_FOUND;
- }
-
- $documentToken = $document->getToken();;
-
- if ($documentToken == $token) {
- return self::TRIPLET_FOUND;
- }
-
- return self::TRIPLET_INVALID;
- }
-
- /**
- * @param mixed $credential
- * @param string $token
- * @param string $persistentToken
- * @param int $expire
- * @return $this
- */
- public function storeTriplet($credential, $token, $persistentToken, $expire = 0)
- {
- // Hash the tokens, because they can contain a salt and can be accessed in the file system
- $persistentToken = sha1($persistentToken);
- $token = sha1($token);
-
- $collection = $this->getDocumentManager()->getCollection('persistentSessions');
-
- $sessionDocument = $collection->createDocument();
-
- $sessionDocument->setCredential($credential);
- $sessionDocument->setToken($token);
- $sessionDocument->setPersistentToken($persistentToken);
-
- $sessionDocument->save();
-
- return $this;
- }
-
- /**
- * @param mixed $credential
- * @param string $persistentToken
- */
- public function cleanTriplet($credential, $persistentToken)
- {
- $persistentToken = sha1($persistentToken);
- $collection = $this->getDocumentManager()->getCollection('persistentSessions');
- $cursor = $collection->find();
-
- $cursor->where('credential', $credential);
- $cursor->where('persistentToken', $persistentToken);
-
- $result = $cursor->findOne();
-
- if ($result) {
- $result->delete();
- }
- }
-
- /**
- * Replace current token after successful authentication
- * @param $credential
- * @param $token
- * @param $persistentToken
- * @param int $expire
- */
- public function replaceTriplet($credential, $token, $persistentToken, $expire = 0)
- {
- $this->cleanTriplet($credential, $persistentToken);
- $this->storeTriplet($credential, $token, $persistentToken, $expire);
- }
-
- /**
- * @param $credential
- */
- public function cleanAllTriplets($credential)
- {
- $collection = $this->getDocumentManager()->getCollection('persistentSessions');
-
- $expression = $collection->expression();
- $expression->where('credential', $credential);
-
- $collection->deleteDocuments($expression);
- }
-
- /**
- * Gets the value of documentManager.
- *
- * @return \Sokil\Mongo\Client
- */
- public function getDocumentManager()
- {
- return $this->documentManager;
- }
-
- /**
- * Sets the value of documentManager.
- *
- * @param \Sokil\Mongo\Client $documentManager the document manager
- *
- * @return self
- */
- public function setDocumentManager(\Sokil\Mongo\Client $documentManager)
- {
- $this->documentManager = $documentManager;
-
- return $this;
- }
-}
\ No newline at end of file
diff --git a/src/xAPI/Util/Versioning.php b/src/xAPI/Util/Versioning.php
index a43b64bd..152fb820 100644
--- a/src/xAPI/Util/Versioning.php
+++ b/src/xAPI/Util/Versioning.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,44 +29,45 @@
class Versioning
{
/**
- * Prefix for class
+ * Prefix for class.
**/
const CLASSPREFIX = 'V';
/**
- * Major version
+ * Major version.
*
* @var int
**/
- private $major = 0;
+ public $major = 0;
/**
- * Minor version
+ * Minor version.
*
* @var int
**/
- private $minor = 0;
+ public $minor = 0;
/**
- * Patch version
+ * Patch version.
*
* @var int
**/
- private $patch = 0;
+ public $patch = 0;
/**
- * Original version String
+ * Original version String.
*
* @var string
*/
- private $originalVersionString;
+ public $originalVersionString;
/**
- * Parse a string into Versionable properties
+ * Parse a string into Versionable properties.
*
* @throws InvalidArgumentException
*
- * @param string $string
+ * @param string $string
+ *
* @return Version
**/
public static function fromString($string)
@@ -74,14 +75,14 @@ public static function fromString($string)
// Sanity check
if (substr_count($string, '.') !== 2) {
throw new InvalidArgumentException(
- 'Version "' . $string . '" can not be parsed into a valid SemVer major.minor.patch version'
+ 'Version "'.$string.'" can not be parsed into a valid SemVer major.minor.patch version'
);
}
$parts = explode('.', $string);
-
+
$versionable = new self();
-
+
// Extra check
if (!is_numeric($parts[0])
|| ((int) $parts[0] < 0)
@@ -91,7 +92,7 @@ public static function fromString($string)
|| ((int) $parts[2] < 0)
) {
throw new InvalidArgumentException(
- 'Version "' . $string . '" can not be parsed into a valid SemVer major.minor.patch version'
+ 'Version "'.$string.'" can not be parsed into a valid SemVer major.minor.patch version'
);
}
@@ -107,7 +108,7 @@ public static function fromString($string)
public function generateClassNamespace()
{
- return (self::CLASSPREFIX . $this->getMajor() . $this->getMinor());
+ return self::CLASSPREFIX.$this->getMajor().$this->getMinor();
}
/**
diff --git a/src/xAPI/Util/xAPI.php b/src/xAPI/Util/xAPI.php
new file mode 100644
index 00000000..47b65e31
--- /dev/null
+++ b/src/xAPI/Util/xAPI.php
@@ -0,0 +1,113 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+namespace API\Util;
+
+class xAPI
+{
+ /**
+ * normalizes xAPI IRI
+ * @param object $obj IRI
+ *
+ * @return string|null
+ */
+ public static function extractUniqueIdentifier($obj)
+ {
+ $uniqueIdentifier = null;
+ $obj = (object) $obj;
+
+ // Fetch the identifier - otherwise we'd have to order the JSON
+ if (isset($obj->mbox)) {
+ $uniqueIdentifier = 'mbox';
+ } elseif (isset($obj->mbox_sha1sum)) {
+ $uniqueIdentifier = 'mbox_sha1sum';
+ } elseif (isset($obj->openid)) {
+ $uniqueIdentifier = 'openid';
+ } elseif (isset($obj->account)) {
+ $uniqueIdentifier = 'account';
+ }
+
+ return $uniqueIdentifier;
+ }
+
+ /**
+ * Extracts xAPI IRI objectType property. Inspects property for allowed values
+ * This function does not validate other IRI properties in relation to objectType
+ * @param object $obj IRI
+ *
+ * @return string|null
+ */
+ public static function extractIriObjectType($obj)
+ {
+ $obj = (object) $obj;
+
+ if (!isset($obj->objectType)) {
+ return 'Agent';
+ }
+
+ // Case sensititive!
+ if (isset($obj->objectType)) {
+ if($obj->objectType == 'Agent') {
+ return 'Agent';
+ }
+ if($obj->objectType == 'Group') {
+ return 'Group';
+ }
+ }
+ // Invalid or falsy objectType values
+ return null;
+ }
+
+ /**
+ * Checks if object has valid Agent ifi properties and returns unique identifier
+ * @param object $obj IRI
+ *
+ * @return string|null
+ */
+ public static function extractAgentIdentifier($obj)
+ {
+ $obj = (object) $obj;
+
+ if (self::extractIriObjectType($obj) !== 'Agent') {
+ return null;
+ }
+
+ if (isset($obj->member)) {
+ return null;
+ }
+
+ return self::extractUniqueIdentifier($obj);
+ }
+
+ /**
+ * Normalizes upercase and legacy UUID patterns (issue#76)
+ * @param string $uuid
+ *
+ * @return string normalized uuid (ready for \Mongo\ObjectId::__constuct())
+ */
+ public static function normalizeUuid($uuid)
+ {
+ return strtolower(str_replace(['urn:', 'uuid:', '{', '}'], '', $uuid));
+ }
+}
diff --git a/src/xAPI/Validator.php b/src/xAPI/Validator.php
index 3b164b61..20ffa212 100644
--- a/src/xAPI/Validator.php
+++ b/src/xAPI/Validator.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,98 +24,147 @@
namespace API;
+use API\BaseTrait;
+use API\Config;
+
use JsonSchema;
-use API\Validator\Exception;
+use API\Validator\JsonSchema\Constraints\Factory as Factory;
+
+use API\HttpException as Exception;
+
-abstract class Validator
+class Validator
{
- /**
- * @var \JsonSchema\Validator
- */
- private $schemaValidator;
+ use BaseTrait;
/**
- * @var \JsonSchema\Uri\UriRetriever
+ * @var JsonSchema\SchemaStorage a persistent SchemaStorage instance
+ * Note that a SchemaStorage instance has an internal cache which takes care of loading and caching files.
*/
- private $retriever;
+ private static $schemaStorage = null;
- /**
- * @var \JsonSchema\RefResolver
- */
- private $refResolver;
+ protected $lastValidator = null;
+ protected $lastSchema = null;
- /**
- * Constructor.
- */
- public function __construct()
- {
- $this->setDefaultSchemaValidator();
- }
+ protected $debug;
+ private $debugData = null;
/**
- * @return \JsonSchema\Validator
- */
- public function getSchemaValidator()
- {
- return $this->schemaValidator;
- }
- /**
- * @param \JsonSchema\Validator $schemaValidator
+ * Constructor, creates and caches a instance.
*/
- public function setSchemaValidator($schemaValidator)
+ public function __construct($container)
{
- $this->schemaValidator = $schemaValidator;
+ $this->setContainer($container);
+
+ if (!self::$schemaStorage) {
+ self::$schemaStorage = new JsonSchema\SchemaStorage();
+ }
+ $this->debug = Config::get('debug', false);
}
/**
- * @return \JsonSchema\RefResolver
+ * Create JsonSchema\Validator instance
+ * @param Factory $factory
+ *
+ * @return JsonSchema\Validator
*/
- public function getSchemaReferenceResolver()
+ public function createSchemaValidator($factory)
{
- return $this->refResolver;
+ return new JsonSchema\Validator($factory);
}
+
/**
- * @param \JsonSchema\RefResolver $refResolver
+ * Validate data with JsonSchema
+ * We intentionally create a new Validator instance on each call.
+ *
+ * @param object|array $data
+ * @param string $uri (with fragment)
+ *
+ * @return JsonSchema\Validator
*/
- public function setSchemaReferenceResolver($refResolver)
+ public function validateSchema($data, $uri)
{
- $this->refResolver = $refResolver;
+ $schema = self::$schemaStorage->getSchema($uri);
+ $validator = new JsonSchema\Validator(new Factory(self::$schemaStorage, null, JsonSchema\Constraints\Constraint::CHECK_MODE_TYPE_CAST));
+ $validator->check($data, $schema);
+
+ if ($this->debug) {
+ $this->debugData = $this->debugSchema($data, $uri, $validator, $schema);
+ }
+
+ return $validator;
}
/**
- * @return \JsonSchema\Uri\UriRetriever
+ * Debug data, validated with JsonSchema.
+ *
+ * @param object|array $data
+ * @param string $uri (with fragment)
+ * @param JsonSchema\Validator $validator
+ * @param object $schema
+ *
+ * @return void
+ * @throws HttpException
*/
- public function getSchemaRetriever()
+ public function debugSchema($data, $uri, $validator, $schema)
{
- return $this->retriever;
+ return [
+ 'hasErrors' => count($validator->getErrors()),
+ 'errors' => ($data) ? $validator->getErrors() : [],
+ 'uri' => $uri,
+ 'schema' => $schema,
+ 'data' => $data,
+ ];
}
+
/**
- * @param \JsonSchema\Uri\UriRetriever $uriRetriever
+ * Performs general validation of the request.
+ *
+ * @return void
+ * @throws Exception
*/
- public function setSchemaRetriever($uriRetriever)
+ public function validateRequest()
{
- $this->retriever = $uriRetriever;
+ $version = $this->getContainer()->get('version');//run version container
}
/**
- * Sets the default schema validator.
+ * Throw errors.
+ *
+ * @param string $message
+ * @param mixed $errors
+ *
+ * @return void
+ * @throws HttpException
*/
- public function setDefaultSchemaValidator()
+ protected function throwErrors($message, $errors)
{
- $this->retriever = new JsonSchema\Uri\UriRetriever();
- $this->refResolver = new JsonSchema\RefResolver($this->retriever);
- $this->schemaValidator = new JsonSchema\Validator();
+ $errors = (array) $errors;
+ throw new Exception($message, Controller::STATUS_BAD_REQUEST, $errors);
}
/**
- * Performs general validation of the request.
+ * Processes and Rendes validator errors in an array.
*
- * @param \Silex\Request $request The request
+ * @param string $message
+ * @param JsonSchema\Validator $validator validator instance, note that you must have validated at this stage
+ *
+ * @return void
+ * @throws HttpException
*/
- public function validateRequest($request)
+ protected function throwSchemaErrors($message, $validator)
{
- if ($request->headers('X-Experience-API-Version') === null) {
- throw new Exception('X-Experience-API-Version header missing.', Resource::STATUS_BAD_REQUEST);
+ $errors = $validator->getErrors();
+ foreach ($errors as $key => $error) {
+ if ($error['property']) {
+ $errors[$key] = sprintf('[%s]: %s', $error['property'], $error['message']);
+ } else {
+ $errors[$key] = sprintf($error['message']);
+ }
+ }
+ if ($this->debug) {
+ $errors['debug'] = $this->debugData;
}
+ throw new Exception($message, Controller::STATUS_BAD_REQUEST, $errors);
}
}
diff --git a/src/xAPI/Validator/Exception.php b/src/xAPI/Validator/Exception.php
index 3ead6bd9..a952c56b 100644
--- a/src/xAPI/Validator/Exception.php
+++ b/src/xAPI/Validator/Exception.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/xAPI/Validator/JsonSchema/Constraints/Factory.php b/src/xAPI/Validator/JsonSchema/Constraints/Factory.php
new file mode 100644
index 00000000..067226fb
--- /dev/null
+++ b/src/xAPI/Validator/JsonSchema/Constraints/Factory.php
@@ -0,0 +1,68 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+
+namespace API\Validator\JsonSchema\Constraints;
+
+use JsonSchema;
+
+/**
+ * Factory for centralize constraint initialization.
+ */
+class Factory extends JsonSchema\Constraints\Factory
+{
+ /**
+ * @var array $customConstraintMap
+ */
+ protected $customConstraints = [
+ 'format' => '\API\Validator\JsonSchema\Constraints\FormatConstraint',
+ ];
+
+ /**
+ *
+ * @param JsonSchema\SchemaStorageInterface $schemaStorage
+ * @param JsonSchema\UriRetrieverInterface $uriRetriever
+ * @param int $checkMode
+ *
+ * @see JsonSchema\Constraints\Factory
+ */
+ public function __construct(
+ JsonSchema\SchemaStorageInterface $schemaStorage = null,
+ JsonSchema\UriRetrieverInterface $uriRetriever = null,
+ $checkMode = JsonSchema\Constraints\Constraint::CHECK_MODE_NORMAL
+ ) {
+ // Merge custom constraints
+ $this->constraintMap = array_merge($this->constraintMap, $this->customConstraints);
+ parent::__construct($schemaStorage, $uriRetriever, $checkMode);
+ }
+
+ /**
+ * Gets merge class map of constraints (for tests)
+ *
+ * @return array $constraintMap
+ */
+ public function getConstraintMap()
+ {
+ return $this->constraintMap;
+ }
+}
diff --git a/src/xAPI/Validator/JsonSchema/Constraints/FormatConstraint.php b/src/xAPI/Validator/JsonSchema/Constraints/FormatConstraint.php
new file mode 100644
index 00000000..57840b9d
--- /dev/null
+++ b/src/xAPI/Validator/JsonSchema/Constraints/FormatConstraint.php
@@ -0,0 +1,91 @@
+.
+ *
+ * For authorship information, please view the AUTHORS
+ * file that was distributed with this source code.
+ */
+
+
+namespace API\Validator\JsonSchema\Constraints;
+
+use JsonSchema;
+use Ramsey\Uuid\Uuid;
+
+/**
+ * Validates against the 'format' property
+ */
+class FormatConstraint extends JsonSchema\Constraints\FormatConstraint
+{
+ /**
+ * Invokes the validation of an element
+ *
+ * @param mixed $value
+ * @param mixed $schema
+ * @param JsonPointer|null $path
+ * @param mixed $i
+ * @throws \JsonSchema\Exception\ExceptionInterface
+ */
+ public function check($element, $schema = null, JsonSchema\Entity\JsonPointer $path = null, $i = null)
+ {
+
+ if (!isset($schema->format)) {
+ return;
+ }
+
+ switch ($schema->format) {
+ // @see https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
+ // @see https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier
+ // @see https://www.w3.org/TR/uri-clarification/
+ // @see http://tools.ietf.org/html/rfc3987
+ case 'iri':
+ if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) {
+ $parsed = parse_url($element);
+ // PHP FILTER_VALIDATE_URL covers only rfc2396, non-ansi characters will fail (i.e. https://fa.wikipedia.org/wiki/یوآرآی)
+ if (false === $parsed) {
+ parent::addError($path, 'Invalid RFC 3987 IRI format', 'format', [ 'format' => $schema->format ]);
+ }
+
+ $scheme = (isset($parsed['scheme'])) ? $parsed['scheme'] : null;
+
+ // empty scheme requires host: //example.org/scheme-relative/URI/with/absolute/path/to/resource)
+ if (!$scheme && empty($parsed['host'])) {
+ parent::addError($path, 'Invalid RFC 3987 IRI format', 'format', [ 'format' => $schema->format ]);
+ }
+
+ // urn:ISSN:1535-3613
+ if ($scheme) {
+ if($scheme === 'urn' && empty($parsed['path'])) {
+ parent::addError($path, 'Invalid RFC 3987 IRI format', 'format', [ 'format' => $schema->format ]);
+ }
+ }
+ }
+ break;
+
+ case 'uuid':
+ if (!Uuid::isValid($element)) {
+ $this->addError($path, 'Invalid UUID', 'format', [ 'format' => $schema->format ]);
+ }
+ break;
+
+ default:
+ parent::check($element, $schema, $path, $i);
+ break;
+ }
+ }
+}
diff --git a/src/xAPI/Validator/V10/Attachment.php b/src/xAPI/Validator/V10/Attachment.php
index e9b48d0a..47f3e4f8 100644
--- a/src/xAPI/Validator/V10/Attachment.php
+++ b/src/xAPI/Validator/V10/Attachment.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/xAPI/Validator/V10/Schema/Statements.json b/src/xAPI/Validator/V10/Schema/Statements.json
index 51fe4da4..0e15c73f 100644
--- a/src/xAPI/Validator/V10/Schema/Statements.json
+++ b/src/xAPI/Validator/V10/Schema/Statements.json
@@ -3,7 +3,7 @@
"description": "Schema for the xAPI Statements endpoint",
"getParameters": {
"type": ["object", "array"],
- "oneOf" : [
+ "anyOf" : [
{
"type": "array",
"maxItems": 0
@@ -15,23 +15,23 @@
"anyOf": [{"required": ["statementId", "voidedStatementId"]}]
},
"properties": {
- "statementId": {"$ref": "Statements.json#/definitions/uuid"},
- "voidedStatementId": {"$ref": "Statements.json#/definitions/uuid"},
+ "statementId": {"$ref": "#/definitions/uuid"},
+ "voidedStatementId": {"$ref": "#/definitions/uuid"},
"agent": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/agent"}, {"$ref": "Statements.json#/definitions/group"}]
+ "oneOf": [{"$ref": "#/definitions/agent"}, {"$ref": "#/definitions/group"}]
},
"verb": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"activity": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"registration": {
"type": "string",
- "oneOf": [{"$ref": "Statements.json#/definitions/uuid"}]
+ "oneOf": [{"$ref": "#/definitions/uuid"}]
},
"related_activities": {
"type": "boolean"
@@ -40,9 +40,17 @@
"type": "boolean"
},
"since": {
- "type": "string"
+ "type": "string",
+ "format": "date-time"
},
"until": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "until_id": {
+ "type": "string"
+ },
+ "since_id": {
"type": "string"
},
"limit": {
@@ -61,12 +69,6 @@
},
"ascending": {
"type": "boolean"
- },
- "until_id": {
- "type": "string"
- },
- "since_id": {
- "type": "string"
}
}
}
@@ -76,11 +78,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/statement"
+ "$ref": "#/definitions/statement"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/statement"}
+ "items": {"$ref": "#/definitions/statement"}
}
]
},
@@ -88,14 +90,14 @@
"type": ["object", "array"],
"required": ["statementId"],
"properties": {
- "statementId": {"$ref": "Statements.json#/definitions/uuid"}
+ "statementId": {"$ref": "#/definitions/uuid"}
}
},
"putBody": {
"type": "object",
"oneOf": [
{
- "$ref": "Statements.json#/definitions/statement"
+ "$ref": "#/definitions/statement"
}
]
},
@@ -104,11 +106,11 @@
"type": "object",
"required": ["actor", "verb", "object"],
"properties": {
- "id": {"$ref": "Statements.json#/definitions/uuid"},
+ "id": {"$ref": "#/definitions/uuid"},
"version": {"type": "string", "enum": ["1.0.0"]},
"actor": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/agent"}, {"$ref": "Statements.json#/definitions/group"}]
+ "oneOf": [{"$ref": "#/definitions/agent"}, {"$ref": "#/definitions/group"}]
},
"verb": {
"type": "object",
@@ -116,10 +118,10 @@
"properties": {
"id": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"display": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
}
},
"additionalProperties": false
@@ -127,11 +129,11 @@
"object": {
"type": "object",
"oneOf": [
- {"$ref": "Statements.json#/definitions/activity"},
- {"$ref": "Statements.json#/definitions/objectagent"},
- {"$ref": "Statements.json#/definitions/group"},
- {"$ref": "Statements.json#/definitions/statementref"},
- {"$ref": "Statements.json#/definitions/substatement"}
+ {"$ref": "#/definitions/activity"},
+ {"$ref": "#/definitions/objectagent"},
+ {"$ref": "#/definitions/group"},
+ {"$ref": "#/definitions/statementref"},
+ {"$ref": "#/definitions/substatement"}
]
},
"objectType": {},
@@ -170,7 +172,7 @@
"type": "string"
},
"extensions": {
- "$ref": "Statements.json#/definitions/extensions"
+ "$ref": "#/definitions/extensions"
}
},
"additionalProperties": false
@@ -180,15 +182,15 @@
"properties": {
"registration": {
"type": "string",
- "oneOf": [{"$ref": "Statements.json#/definitions/uuid"}]
+ "oneOf": [{"$ref": "#/definitions/uuid"}]
},
"instructor": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/agent"}, {"$ref": "Statements.json#/definitions/group"}]
+ "oneOf": [{"$ref": "#/definitions/agent"}, {"$ref": "#/definitions/group"}]
},
"team": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/agent"}, {"$ref": "Statements.json#/definitions/group"}]
+ "oneOf": [{"$ref": "#/definitions/agent"}, {"$ref": "#/definitions/group"}]
},
"contextActivities": {
"type": "object",
@@ -197,11 +199,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/activity"
+ "$ref": "#/definitions/activity"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/activity"}
+ "items": {"$ref": "#/definitions/activity"}
}
]
},
@@ -209,11 +211,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/activity"
+ "$ref": "#/definitions/activity"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/activity"}
+ "items": {"$ref": "#/definitions/activity"}
}
]
},
@@ -221,11 +223,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/activity"
+ "$ref": "#/definitions/activity"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/activity"}
+ "items": {"$ref": "#/definitions/activity"}
}
]
},
@@ -233,11 +235,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/activity"
+ "$ref": "#/definitions/activity"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/activity"}
+ "items": {"$ref": "#/definitions/activity"}
}
]
}
@@ -254,10 +256,10 @@
"type": "string"
},
"statement": {
- "$ref": "Statements.json#/definitions/statementref"
+ "$ref": "#/definitions/statementref"
},
"extensions": {
- "$ref": "Statements.json#/definitions/extensions"
+ "$ref": "#/definitions/extensions"
}
},
"additionalProperties": false
@@ -271,10 +273,10 @@
"authority": {
"type": "object",
"oneOf": [
- {"$ref": "Statements.json#/definitions/agent"},
+ {"$ref": "#/definitions/agent"},
{
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/anonymousgroup"}],
+ "oneOf": [{"$ref": "#/definitions/anonymousgroup"}],
"properties": {
"member": {
"type": "array",
@@ -291,7 +293,7 @@
"attachments":
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/attachment"}
+ "items": {"$ref": "#/definitions/attachment"}
}
},
"additionalProperties": false
@@ -303,13 +305,13 @@
"objectType": {
"enum": ["StatementRef"]
},
- "id": {"$ref": "Statements.json#/definitions/uuid"}
+ "id": {"$ref": "#/definitions/uuid"}
},
"additionalProperties": false
},
"substatement": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/statement"}],
+ "oneOf": [{"$ref": "#/definitions/statement"}],
"required": ["objectType"],
"properties": {
"objectType": {
@@ -345,7 +347,7 @@
"properties": {
"id": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"objectType": {
"enum": ["Activity"]
@@ -354,14 +356,14 @@
"type": "object",
"properties": {
"name": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
},
"description": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
},
"type": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"moreInfo": {
"type": "string",
@@ -388,7 +390,7 @@
}
},
"extensions": {
- "$ref": "Statements.json#/definitions/extensions"
+ "$ref": "#/definitions/extensions"
}
},
"patternProperties": {
@@ -396,11 +398,11 @@
"type": ["object", "array"],
"oneOf" : [
{
- "$ref": "Statements.json#/definitions/interaction"
+ "$ref": "#/definitions/interaction"
},
{
"type": "array",
- "items": {"$ref": "Statements.json#/definitions/interaction"}
+ "items": {"$ref": "#/definitions/interaction"}
}
]
}
@@ -501,11 +503,11 @@
},
"inversefunctional": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/mbox"}, {"$ref": "Statements.json#/definitions/mbox_sha1sum"}, {"$ref": "Statements.json#/definitions/account"}, {"$ref": "Statements.json#/definitions/openid"}]
+ "oneOf": [{"$ref": "#/definitions/mbox"}, {"$ref": "#/definitions/mbox_sha1sum"}, {"$ref": "#/definitions/account"}, {"$ref": "#/definitions/openid"}]
},
"objectagent": {
"type": "object",
- "oneOf": [{"$ref": "Statements.json#/definitions/agent"}],
+ "oneOf": [{"$ref": "#/definitions/agent"}],
"required": ["objectType"],
"properties": {
"objectType": {
@@ -532,19 +534,16 @@
"additionalProperties": false
},
{
- "oneOf": [{"$ref": "Statements.json#/definitions/mbox"}, {"$ref": "Statements.json#/definitions/mbox_sha1sum"}, {"$ref": "Statements.json#/definitions/account"}, {"$ref": "Statements.json#/definitions/openid"}],
- "additionalProperties": false
- },
- {
- "additionalProperties": false
+ "oneOf": [{"$ref": "#/definitions/mbox"}, {"$ref": "#/definitions/mbox_sha1sum"}, {"$ref": "#/definitions/account"}, {"$ref": "#/definitions/openid"}],
+ "additionalProperties": true
}
]
},
"group": {
"type": "object",
"oneOf": [
- { "$ref": "Statements.json#/definitions/anonymousgroup" },
- { "$ref": "Statements.json#/definitions/identifiedgroup" }
+ { "$ref": "#/definitions/anonymousgroup" },
+ { "$ref": "#/definitions/identifiedgroup" }
]
},
"anonymousgroup": {
@@ -559,7 +558,7 @@
"member": {
"type": "array",
"minItems": 1,
- "items": {"$ref": "Statements.json#/definitions/agent"}
+ "items": {"$ref": "#/definitions/agent"}
}
},
"additionalProperties": false
@@ -578,7 +577,7 @@
"member": {
"type": "array",
"minItems": 1,
- "items": {"$ref": "Statements.json#/definitions/agent"}
+ "items": {"$ref": "#/definitions/agent"}
},
"mbox": {},
"mbox_sha1sum": {},
@@ -588,17 +587,14 @@
"additionalProperties": false
},
{
- "oneOf": [{"$ref": "Statements.json#/definitions/mbox"}, {"$ref": "Statements.json#/definitions/mbox_sha1sum"}, {"$ref": "Statements.json#/definitions/account"}, {"$ref": "Statements.json#/definitions/openid"}],
- "additionalProperties": false
- },
- {
- "additionalProperties": false
+ "oneOf": [{"$ref": "#/definitions/mbox"}, {"$ref": "#/definitions/mbox_sha1sum"}, {"$ref": "#/definitions/account"}, {"$ref": "#/definitions/openid"}],
+ "additionalProperties": true
}
]
},
"uuid": {
"type": "string",
- "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
+ "format": "uuid"
},
"attachment": {
"type": "object",
@@ -606,13 +602,13 @@
"properties": {
"usageType": {
"type": "string",
- "format": "uri"
+ "format": "iri"
},
"display": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
},
"description": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
},
"contentType": {
"type": "string"
@@ -638,7 +634,7 @@
"type": "string"
},
"description": {
- "$ref": "Statements.json#/definitions/languagemap"
+ "$ref": "#/definitions/languagemap"
}
},
"additionalProperties": false
diff --git a/src/xAPI/Validator/V10/Statement.php b/src/xAPI/Validator/V10/Statement.php
index 71ae31dd..9076e14d 100644
--- a/src/xAPI/Validator/V10/Statement.php
+++ b/src/xAPI/Validator/V10/Statement.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2016 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,90 +25,64 @@
namespace API\Validator\V10;
use API\Validator;
-use API\Resource;
-use API\Validator\Exception;
class Statement extends Validator
{
- protected function retrieveByFragment($fragment)
+ protected function validateBySchemaFragment($data, $fragment, $debug = false)
{
- $schema = $this->getSchemaRetriever()->retrieve('file://'.__DIR__.'/Schema/Statements.json#'.$fragment);
+ $fragment = ($fragment) ? '#'.$fragment : '';
- return $schema;
- }
-
- protected function throwErrors($message, $errors)
- {
- $message .= ' Violations: ';
- foreach ($errors as $error) {
- $message .= sprintf("[%s] %s\n", $error['property'], $error['message']);
- }
- throw new Exception($message, Resource::STATUS_BAD_REQUEST);
+ return $this->validateSchema($data, 'file://'.__DIR__.'/Schema/Statements.json'.$fragment, $debug);
}
// Handles the validation of GET /statements
- public function validateGetRequest($request)
+ public function validateGetRequest()
{
- $data = $request->get();
+ $data = $this->getContainer()->get('parser')->getData()->getParameters();
foreach ($data as $key => $value) {
$decodedValue = json_decode($value);
if (json_last_error() == JSON_ERROR_NONE) {
$data[$key] = $decodedValue;
- }
+ }
}
if (!empty($data)) {
$data = (object) $data;
}
- $schema = $this->retrieveByFragment('getParameters');
- $this->getSchemaReferenceResolver()->resolve($schema);
- $this->getSchemaValidator()->check($data, $schema);
-
- if (!$this->getSchemaValidator()->isValid()) {
- $this->throwErrors('GET parameters do not validate.', $this->getSchemaValidator()->getErrors());
+ $validator = $this->validateBySchemaFragment($data, 'getParameters');
+ if (!$validator->isValid()) {
+ $this->throwSchemaErrors('GET parameters do not validate.', $validator);
}
}
// POST-ing a statement validation
- public function validatePostRequest($request)
+ public function validatePostRequest()
{
- // Then do specific validation
- $data = $request->getBody();
- $data = json_decode($data);
-
- $schema = $this->retrieveByFragment('postBody');
- $this->getSchemaReferenceResolver()->resolve($schema);
- $this->getSchemaValidator()->check($data, $schema);
+ $data = $this->getContainer()->get('parser')->getData()->getPayload();
- if (!$this->getSchemaValidator()->isValid()) {
- $this->throwErrors('Statements do not validate.', $this->getSchemaValidator()->getErrors());
+ $validator = $this->validateBySchemaFragment($data, 'postBody');
+ if (!$validator->isValid()) {
+ $this->throwSchemaErrors('Statements do not validate.', $validator);
}
}
// PUT-ing one or more statements validation
- public function validatePutRequest($request)
+ public function validatePutRequest()
{
// Then do specific validation
- $data = $request->get();
- $schema = $this->retrieveByFragment('putParameters');
- $this->getSchemaReferenceResolver()->resolve($schema);
- $this->getSchemaValidator()->check($data, $schema);
-
- if (!$this->getSchemaValidator()->isValid()) {
- $this->throwErrors('PUT parameters do not validate.', $this->getSchemaValidator()->getErrors());
+ $data = $this->getContainer()->get('parser')->getData()->getParameters();
+ $validator = $this->validateBySchemaFragment($data, 'putParameters');
+ if (!$validator->isValid()) {
+ $this->throwSchemaErrors('PUT parameters do not validate.', $validator);
}
- $data = $request->getBody();
- $data = json_decode($data);
-
- $schema = $this->retrieveByFragment('putBody');
- $this->getSchemaReferenceResolver()->resolve($schema);
- $this->getSchemaValidator()->check($data, $schema);
+ $data = $this->getContainer()->get('parser')->getData()->getPayload();
- if (!$this->getSchemaValidator()->isValid()) {
- $this->throwErrors('Statements do not validate.', $this->getSchemaValidator()->getErrors());
+ $validator = $this->validateBySchemaFragment($data, 'putBody');
+ if (!$validator->isValid()) {
+ $this->throwSchemaErrors('Statements do not validate.', $validator);
}
}
}
diff --git a/src/xAPI/View.php b/src/xAPI/View.php
index 4f47626d..f75cf149 100644
--- a/src/xAPI/View.php
+++ b/src/xAPI/View.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,36 +24,72 @@
namespace API;
-use Slim\Slim;
+use API\Util\Collection;
-abstract class View extends \Slim\Helper\Set
+abstract class View extends Collection
{
+ use BaseTrait;
+
+ protected $items;
+
+ protected $response;
+
/**
- * @var \Slim\Slim
+ * @constructor
*/
- private $slim;
+ public function __construct($response, $container, $items = [])
+ {
+ parent::__construct($items);
+ $this->setResponse($response);
+ $this->setContainer($container);
+ $this->items = $items;
+ }
/**
- * Construct.
+ * Gets items.
+ *
+ * @return mixed
*/
- public function __construct($items = [])
+ public function getItems()
{
- parent::__construct($items);
- $this->setSlim(Slim::getInstance());
+ return $this->items;
}
/**
- * @return \Slim\Slim
+ * Gets response.
+ *
+ * @return mixed
*/
- public function getSlim()
+ public function getResponse()
{
- return $this->slim;
+ return $this->response;
}
+
/**
- * @param \Slim\Slim $slim
+ * Sets the items.
+ *
+ * @param mixed $items the items
+ *
+ * @return self
*/
- public function setSlim($slim)
+ protected function setItems($items)
{
- $this->slim = $slim;
+ $this->items = $items;
+
+ return $this;
+ }
+
+ /**
+ * Sets the response.
+ *
+ * @param mixed $response the response
+ *
+ * @return self
+ */
+ protected function setResponse($response)
+ {
+ $this->response = $response;
+
+ return $this;
}
}
diff --git a/src/xAPI/View/V10/About.php b/src/xAPI/View/V10/About.php
index bfea5131..da9c5316 100644
--- a/src/xAPI/View/V10/About.php
+++ b/src/xAPI/View/V10/About.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,8 +32,11 @@ class About extends View
{
public function render()
{
- $object = ['version' => $this->versions];
+ $object = [
+ 'version' => $this->versions,
+ 'extensions' => $this->extensions
+ ];
return $object;
}
-}
\ No newline at end of file
+}
diff --git a/src/xAPI/View/V10/Activity.php b/src/xAPI/View/V10/Activity.php
index cb9a0014..24fb3fe0 100644
--- a/src/xAPI/View/V10/Activity.php
+++ b/src/xAPI/View/V10/Activity.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,11 +28,17 @@
class Activity extends View
{
- public function renderGetSingle()
+ /**
+ * Render the activity
+ * @param stdClass $activityDocument Activity as an object
+ * @return stdClass Modified activity
+ */
+ public function renderGetSingle($activityDocument)
{
- $document = $this->service->getCursor()->current();
- $document->unsetField('_id');
+ if (isset($activityDocument->_id)) {
+ unset($activityDocument->_id);
+ }
- return $document;
+ return $activityDocument;
}
}
diff --git a/src/xAPI/View/V10/ActivityProfile.php b/src/xAPI/View/V10/ActivityProfile.php
index 3ae9c05f..88da9976 100644
--- a/src/xAPI/View/V10/ActivityProfile.php
+++ b/src/xAPI/View/V10/ActivityProfile.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,4 +28,5 @@
class ActivityProfile extends BaseDocument
{
+ const IDENTIFIER = 'profileId';
}
diff --git a/src/xAPI/View/V10/ActivityState.php b/src/xAPI/View/V10/ActivityState.php
index c044e8f3..a55a24bc 100644
--- a/src/xAPI/View/V10/ActivityState.php
+++ b/src/xAPI/View/V10/ActivityState.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,4 +28,5 @@
class ActivityState extends BaseDocument
{
+ const IDENTIFIER = 'stateId';
}
diff --git a/src/xAPI/View/V10/Agent.php b/src/xAPI/View/V10/Agent.php
index eea240b8..c41f488e 100644
--- a/src/xAPI/View/V10/Agent.php
+++ b/src/xAPI/View/V10/Agent.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,13 +25,13 @@
namespace API\View\V10;
use API\View;
-use Slim\Helper\Set;
+use API\Util\Collection;
class Agent extends View
{
- public function renderGet()
+ public function renderGet($agent)
{
- $agent = new Set($this->agent);
+ $agent = new Collection($agent);
$object = ['objectType' => 'Person'];
if ($agent->has('name')) {
diff --git a/src/xAPI/View/V10/AgentProfile.php b/src/xAPI/View/V10/AgentProfile.php
index a649226c..d09bc5d8 100644
--- a/src/xAPI/View/V10/AgentProfile.php
+++ b/src/xAPI/View/V10/AgentProfile.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2016 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,4 +28,5 @@
class AgentProfile extends BaseDocument
{
+ const IDENTIFIER = 'profileId';
}
diff --git a/src/xAPI/View/V10/BaseDocument.php b/src/xAPI/View/V10/BaseDocument.php
index f4179d62..dd4fdf98 100644
--- a/src/xAPI/View/V10/BaseDocument.php
+++ b/src/xAPI/View/V10/BaseDocument.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,29 +26,34 @@
use API\View;
-class BaseDocument extends View
+abstract class BaseDocument extends View
{
- public function renderGet()
+ public function renderGet($documentResult)
{
- $idArray = [];
+ $idArray = [];
- $cursor = $this->service->getCursor();
+ $cursor = $documentResult->getCursor();
foreach ($cursor as $document) {
- $idArray[] = $document->getIdentifier();
+ $idArray[] = $document->{static::IDENTIFIER};
}
return $idArray;
}
- public function renderGetSingle()
+ public function renderGetSingle($documentResult)
{
- $document = $this->service->getCursor()->current();
+ $document = current($documentResult->getCursor()->toArray());
+ $document = new \API\Document\Generic($document);
$content = $document->getContent();
- $this->getSlim()->response->headers->set('ETag', '"'.$document->getHash().'"'); //Quotes required - RFC2616 3.11
- $this->getSlim()->response->headers->set('Content-Type', $document->getContentType());
+ // Write content
+ $newResponse = $this->getResponse()->withHeader('ETag', '"'.$document->getHash().'"')
+ ->withHeader('Content-Type', $document->getContentType());
+ // Write body
+ $body = $newResponse->getBody();
+ $body->write($content);
- return $content;
+ return $newResponse;
}
}
diff --git a/src/xAPI/View/V10/BasicAuth/AccessToken.php b/src/xAPI/View/V10/BasicAuth/AccessToken.php
index 685e5537..45b1057f 100644
--- a/src/xAPI/View/V10/BasicAuth/AccessToken.php
+++ b/src/xAPI/View/V10/BasicAuth/AccessToken.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,22 +25,21 @@
namespace API\View\V10\BasicAuth;
use API\View;
+use API\Util;
class AccessToken extends View
{
- public function render()
+ public function render($accessTokenDocument)
{
- $accessTokenDocument = $this->service->getAccessTokens()[0];
-
$view = [
- 'key' => $accessTokenDocument->getKey(),
- 'secret' => $accessTokenDocument->getSecret(),
- 'expiresAt' => (null === $accessTokenDocument->getExpiresAt()) ? null : $accessTokenDocument->getExpiresAt()->sec,
+ 'key' => $accessTokenDocument->getKey(),
+ 'secret' => $accessTokenDocument->getSecret(),
+ 'expiresAt' => (null === $accessTokenDocument->getExpiresAt()) ? null : Util\Date::mongoDateToTimestamp($accessTokenDocument->getExpiresAt()),
'expiresIn' => $accessTokenDocument->getExpiresIn(),
- 'createdAt' => (null === $accessTokenDocument->getCreatedAt()) ? null : $accessTokenDocument->getCreatedAt()->sec,
- 'expired' => $accessTokenDocument->isExpired(),
- 'scopes' => array_values($accessTokenDocument->scopes),
- 'user' => $accessTokenDocument->user->renderSummary(),
+ 'createdAt' => (null === $accessTokenDocument->getCreatedAt()) ? null : Util\Date::mongoDateToTimestamp($accessTokenDocument->getCreatedAt()),
+ 'expired' => $accessTokenDocument->isExpired(),
+ //'scopes' => array_values($accessTokenDocument->scopes),
+ //'user' => $accessTokenDocument->user->renderSummary(),
];
return $view;
diff --git a/src/xAPI/View/V10/OAuth/AccessToken.php b/src/xAPI/View/V10/OAuth/AccessToken.php
index e131627e..d7c7ddb9 100644
--- a/src/xAPI/View/V10/OAuth/AccessToken.php
+++ b/src/xAPI/View/V10/OAuth/AccessToken.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,33 +30,19 @@
class AccessToken extends View
{
- public function render()
+ public function render($accessTokenDocument)
{
- $accessTokenDocument = $this->service->getAccessTokens()[0];
-
$view = [
'token' => $accessTokenDocument->getToken(),
- 'expiresAt' => (null === $accessTokenDocument->getExpiresAt()) ? null : $accessTokenDocument->getExpiresAt()->sec,
+ 'expiresAt' => (null === $accessTokenDocument->getExpiresAt()) ? null : $accessTokenDocument->getExpiresAt()->toDateTime()->getTimestamp(),
'expiresIn' => $accessTokenDocument->getExpiresIn(),
- 'createdAt' => (null === $accessTokenDocument->getCreatedAt()) ? null : $accessTokenDocument->getCreatedAt()->sec,
+ 'createdAt' => (null === $accessTokenDocument->getCreatedAt()) ? null : $accessTokenDocument->getCreatedAt()->toDateTime()->getTimestamp(),
'expired' => $accessTokenDocument->isExpired(),
- 'scopes' => array_values($accessTokenDocument->scopes),
- 'user' => $accessTokenDocument->user->renderSummary(),
- 'client' => $accessTokenDocument->client->renderSummary(),
+ //'scopes' => array_values($accessTokenDocument->scopes),
+ //'user' => $accessTokenDocument->user->renderSummary(),
+ //'client' => $accessTokenDocument->client->renderSummary(),
];
return $view;
}
-
- public function renderGet()
- {
- // POST is same as GET
- return $this->render();
- }
-
- public function renderPost()
- {
- // POST is same as GET
- return $this->render();
- }
}
diff --git a/src/xAPI/View/V10/OAuth/Authorize.php b/src/xAPI/View/V10/OAuth/Authorize.php
index ab109ccc..5013ea54 100644
--- a/src/xAPI/View/V10/OAuth/Authorize.php
+++ b/src/xAPI/View/V10/OAuth/Authorize.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,20 +25,22 @@
namespace API\View\V10\OAuth;
use API\View;
+use API\Config;
class Authorize extends View
{
- public function renderGet()
+ public function renderGet($user, $client, $scopes)
{
- $view = $this->getSlim()->view;
- $view->setTemplatesDirectory(dirname(__FILE__).'/Templates');
- $this->set('csrfToken', $_SESSION['csrfToken']);
- $this->set('name', $this->getSlim()->config('name'));
- $this->set('branding', $this->getSlim()->config('xAPI')['oauth']['branding']);
- $output = $view->render('authorize.twig', $this->all());
-
- // Set Content-Type to html
- $this->getSlim()->response->headers->set('Content-Type', 'text/html');
+ $view = $this->getContainer()->get('view');
+ $this->setItems(['csrfToken' => $_SESSION['csrfToken'],
+ 'name' => Config::get(['name']),
+ 'branding' => Config::get(['xAPI', 'oauth', 'branding']),
+ 'user' => $user,
+ 'client' => $client,
+ 'scopes' => $scopes
+ ]);
+ $response = $this->getResponse()->withHeader('Content-Type', 'text/html');
+ $output = $view->render($response, 'authorize.twig', $this->getItems());
return $output;
}
diff --git a/src/xAPI/View/V10/OAuth/Login.php b/src/xAPI/View/V10/OAuth/Login.php
index e4e1c8b3..3f7ccfa1 100644
--- a/src/xAPI/View/V10/OAuth/Login.php
+++ b/src/xAPI/View/V10/OAuth/Login.php
@@ -3,7 +3,7 @@
/*
* This file is part of lxHive LRS - http://lxhive.org/
*
- * Copyright (C) 2015 Brightcookie Pty Ltd
+ * Copyright (C) 2017 Brightcookie Pty Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,20 +25,17 @@
namespace API\View\V10\OAuth;
use API\View;
+use API\Config;
class Login extends View
{
- public function renderGet()
+ public function renderGet($errors)
{
- $view = $this->getSlim()->view;
- $view->setTemplatesDirectory(dirname(__FILE__).'/Templates');
- $this->set('csrfToken', $_SESSION['csrfToken']);
- $this->set('name', $this->getSlim()->config('name'));
- $this->set('branding', $this->getSlim()->config('xAPI')['oauth']['branding']);
- $output = $view->render('login.twig', $this->all());
+ $view = $this->getContainer()->get('view');
+ $this->setItems(['csrfToken' => $_SESSION['csrfToken'], 'name' => Config::get(['name']), 'branding' => Config::get(['xAPI', 'oauth', 'branding']), 'errors' => $errors]);
+ $response = $this->getResponse()->withHeader('Content-Type', 'text/html');
+ $output = $view->render($response, 'login.twig', $this->getItems());
- // Set Content-Type to html
- $this->getSlim()->response->headers->set('Content-Type', 'text/html');
return $output;
}
diff --git a/src/xAPI/View/V10/OAuth/Templates/authorize.twig b/src/xAPI/View/V10/OAuth/Templates/authorize.twig
index 6195101b..45431e7c 100644
--- a/src/xAPI/View/V10/OAuth/Templates/authorize.twig
+++ b/src/xAPI/View/V10/OAuth/Templates/authorize.twig
@@ -19,25 +19,17 @@
{% endif %}
- {% if service.errors %}
-
- {% for error in service.errors %}
- {{ error }}
- {% endfor %}
-
- {% endif %}
-