\n',tagBefore2:'
\n"},previewContentTemplates:{generic:"{content}\n",html:'
{data}
\n',image:'
\n',text:'
\n',office:'
',gdocs:'
',video:n,audio:s,flash:'
\n',object:o,pdf:l,other:d},allowedPreviewTypes:["image","html","text","video","audio","flash","pdf","object"],previewTemplates:{},previewSettings:{image:{width:"auto",height:"auto","max-width":"100%","max-height":"100%"},html:{width:"213px",height:"160px"},text:{width:"213px",height:"160px"},office:{width:"213px",height:"160px"},gdocs:{width:"213px",height:"160px"},video:{width:"213px",height:"160px"},audio:{width:"100%",height:"30px"},flash:{width:"213px",height:"160px"},object:{width:"213px",height:"160px"},pdf:{width:"100%",height:"160px"},other:{width:"213px",height:"160px"}},previewSettingsSmall:{image:{width:"auto",height:"auto","max-width":"100%","max-height":"100%"},html:{width:"100%",height:"160px"},text:{width:"100%",height:"160px"},office:{width:"100%",height:"160px"},gdocs:{width:"100%",height:"160px"},video:{width:"100%",height:"auto"},audio:{width:"100%",height:"30px"},flash:{width:"100%",height:"auto"},object:{width:"100%",height:"auto"},pdf:{width:"100%",height:"160px"},other:{width:"100%",height:"160px"}},previewZoomSettings:{image:{width:"auto",height:"auto","max-width":"100%","max-height":"100%"},html:c,text:c,office:{width:"100%",height:"100%","max-width":"100%","min-height":"480px"},gdocs:{width:"100%",height:"100%","max-width":"100%","min-height":"480px"},video:{width:"auto",height:"100%","max-width":"100%"},audio:{width:"100%",height:"30px"},flash:{width:"auto",height:"480px"},object:{width:"auto",height:"100%","max-width":"100%","min-height":"480px"},pdf:c,other:{width:"auto",height:"100%","min-height":"480px"}},fileTypeSettings:{image:function(e,i){return t.compare(e,"image.*")&&!t.compare(e,/(tiff?|wmf)$/i)||t.compare(i,/\.(gif|png|jpe?g)$/i)},html:function(e,i){return t.compare(e,"text/html")||t.compare(i,/\.(htm|html)$/i)},office:function(e,i){return t.compare(e,/(word|excel|powerpoint|office)$/i)||t.compare(i,/\.(docx?|xlsx?|pptx?|pps|potx?)$/i)},gdocs:function(e,i){return t.compare(e,/(word|excel|powerpoint|office|iwork-pages|tiff?)$/i)||t.compare(i,/\.(docx?|xlsx?|pptx?|pps|potx?|rtf|ods|odt|pages|ai|dxf|ttf|tiff?|wmf|e?ps)$/i)},text:function(e,i){return t.compare(e,"text.*")||t.compare(i,/\.(xml|javascript)$/i)||t.compare(i,/\.(txt|md|csv|nfo|ini|json|php|js|css)$/i)},video:function(e,i){return t.compare(e,"video.*")&&(t.compare(e,/(ogg|mp4|mp?g|mov|webm|3gp)$/i)||t.compare(i,/\.(og?|mp4|webm|mp?g|mov|3gp)$/i))},audio:function(e,i){return t.compare(e,"audio.*")&&(t.compare(i,/(ogg|mp3|mp?g|wav)$/i)||t.compare(i,/\.(og?|mp3|mp?g|wav)$/i))},flash:function(e,i){return t.compare(e,"application/x-shockwave-flash",!0)||t.compare(i,/\.(swf)$/i)},pdf:function(e,i){return t.compare(e,"application/pdf",!0)||t.compare(i,/\.(pdf)$/i)},object:function(){return!0},other:function(){return!0}},fileActionSettings:{showRemove:!0,showUpload:!0,showDownload:!0,showZoom:!0,showDrag:!0,removeIcon:' ',removeClass:"btn btn-sm btn-kv btn-default btn-outline-secondary",removeErrorClass:"btn btn-sm btn-kv btn-danger",removeTitle:"Remove file",uploadIcon:' ',uploadClass:"btn btn-sm btn-kv btn-default btn-outline-secondary",uploadTitle:"Upload file",uploadRetryIcon:' ',uploadRetryTitle:"Retry upload",downloadIcon:' ',downloadClass:"btn btn-sm btn-kv btn-default btn-outline-secondary",downloadTitle:"Download file",zoomIcon:' ',zoomClass:"btn btn-sm btn-kv btn-default btn-outline-secondary",zoomTitle:"View Details",dragIcon:' ',dragClass:"text-info",dragTitle:"Move / Rearrange",dragSettings:{},indicatorNew:' ',indicatorSuccess:' ',indicatorError:' ',indicatorLoading:' ',indicatorNewTitle:"Not uploaded yet",indicatorSuccessTitle:"Uploaded",indicatorErrorTitle:"Upload Error",indicatorLoadingTitle:"Uploading ..."}},e.each(h.defaults,function(t,i){"allowedPreviewTypes"!==t?h[t]=e.extend(!0,{},i,h[t]):void 0===h.allowedPreviewTypes&&(h.allowedPreviewTypes=i)}),h._initPreviewTemplates()},_initPreviewTemplates:function(){var i,a=this,r=a.previewMarkupTags,n=r.tagAfter;e.each(a.previewContentTemplates,function(e,s){t.isEmpty(a.previewTemplates[e])&&(i=r.tagBefore2,"generic"!==e&&"image"!==e&&"html"!==e&&"text"!==e||(i=r.tagBefore1),a._isPdfRendered()&&"pdf"===e&&(i=i.replace("kv-file-content","kv-file-content kv-pdf-rendered")),a.previewTemplates[e]=i+s+n)})},_initPreviewCache:function(){var i=this;i.previewCache={data:{},init:function(){var e=i.initialPreview;e.length>0&&!t.isArray(e)&&(e=e.split(i.initialPreviewDelimiter)),i.previewCache.data={content:e,config:i.initialPreviewConfig,tags:i.initialPreviewThumbTags}},count:function(){return i.previewCache.data&&i.previewCache.data.content?i.previewCache.data.content.length:0},get:function(a,r){var n,s,o,l,d,c,h,p="init_"+a,u=i.previewCache.data,f=u.config[a],m=u.content[a],g=i.previewInitId+"-"+p,v=t.ifSet("previewAsData",f,i.initialPreviewAsData),w=function(e,a,r,n,s,o,l,d,c){return d=" file-preview-initial "+t.SORT_CSS+(d?" "+d:""),i._generatePreviewTemplate(e,a,r,n,s,!1,null,d,o,l,c)};return m?(r=void 0===r||r,o=t.ifSet("type",f,i.initialPreviewFileType||"generic"),d=t.ifSet("filename",f,t.ifSet("caption",f)),c=t.ifSet("filetype",f,o),l=i.previewCache.footer(a,r,f&&f.size||null),h=t.ifSet("frameClass",f),n=v?w(o,m,d,c,g,l,p,h):w("generic",m,d,c,g,l,p,h,o).setTokens({content:u.content[a]}),u.tags.length&&u.tags[a]&&(n=t.replaceTags(n,u.tags[a])),t.isEmpty(f)||t.isEmpty(f.frameAttr)||((s=e(document.createElement("div")).html(n)).find(".file-preview-initial").attr(f.frameAttr),n=s.html(),s.remove()),n):""},add:function(e,a,r,n){var s,o=i.previewCache.data;return t.isArray(e)||(e=e.split(i.initialPreviewDelimiter)),n?(s=o.content.push(e)-1,o.config[s]=a,o.tags[s]=r):(s=e.length-1,o.content=e,o.config=a,o.tags=r),i.previewCache.data=o,s},set:function(e,a,r,n){var s,o=i.previewCache.data;if(e&&e.length&&(t.isArray(e)||(e=e.split(i.initialPreviewDelimiter)),e.filter(function(e){return null!==e}).length)){if(void 0===o.content&&(o.content=[]),void 0===o.config&&(o.config=[]),void 0===o.tags&&(o.tags=[]),n){for(s=0;s'+e+"":""+e+" ";return 0===a.find("ul").length?this._addError(""):a.find("ul").append(n),a.fadeIn(800),this._raise(r,[t,e]),this._setValidationError("file-input-new"),!0},_showError:function(e,t,i){var a=this.$errorContainer,r=i||"fileerror";return(t=t||{}).reader=this.reader,this._addError(e),a.fadeIn(800),this._raise(r,[t,e]),this.isAjaxUpload||this._clearFileInput(),this._setValidationError("file-input-new"),this.$btnUpload.attr("disabled",!0),!0},_noFilesError:function(e){var t=this.minFileCount>1?this.filePlural:this.fileSingle,i=this.msgFilesTooLess.replace("{n}",this.minFileCount).replace("{files}",t),a=this.$errorContainer;this._addError(i),this.isError=!0,this._updateFileDetails(0),a.fadeIn(800),this._raise("fileerror",[e,i]),this._clearFileInput(),this._setValidationError()},_parseError:function(t,i,a,r){var n,s=e.trim(a+""),o=void 0!==i.responseJSON&&void 0!==i.responseJSON.error?i.responseJSON.error:i.responseText;return this.cancelling&&this.msgUploadAborted&&(s=this.msgUploadAborted),this.showAjaxErrorDetails&&o&&(n=(o=e.trim(o.replace(/\n\s*\n/g,"\n"))).length?""+o+" ":"",s+=s?n:o),s||(s=this.msgAjaxError.replace("{operation}",t)),this.cancelling=!1,r?""+r+": "+s:s},_parseFileType:function(e,i){var a,r,n,s=this.allowedPreviewTypes||[];if("application/text-plain"===e)return"text";for(n=0;n-1&&(i=t.split(".").pop(),a.previewFileIconSettings&&(r=a.previewFileIconSettings[i]||a.previewFileIconSettings[i.toLowerCase()]||null),a.previewFileExtSettings&&e.each(a.previewFileExtSettings,function(e,t){a.previewFileIconSettings[e]&&t(i)&&(r=a.previewFileIconSettings[e])})),r},_parseFilePreviewIcon:function(e,t){var i=this._getPreviewIcon(t)||this.previewFileIcon,a=e;return a.indexOf("{previewFileIcon}")>-1&&(a=a.setTokens({previewFileIconClass:this.previewFileIconClass,previewFileIcon:i})),a},_raise:function(t,i){var a=e.Event(t);if(void 0!==i?this.$element.trigger(a,i):this.$element.trigger(a),a.isDefaultPrevented()||!1===a.result)return!1;switch(t){case"filebatchuploadcomplete":case"filebatchuploadsuccess":case"fileuploaded":case"fileclear":case"filecleared":case"filereset":case"fileerror":case"filefoldererror":case"fileuploaderror":case"filebatchuploaderror":case"filedeleteerror":case"filecustomerror":case"filesuccessremove":break;default:this.ajaxAborted||(this.ajaxAborted=a.result)}return!0},_listenFullScreen:function(e){var t,i,a=this.$modal;a&&a.length&&(t=a&&a.find(".btn-fullscreen"),i=a&&a.find(".btn-borderless"),t.length&&i.length&&(t.removeClass("active").attr("aria-pressed","false"),i.removeClass("active").attr("aria-pressed","false"),e?t.addClass("active").attr("aria-pressed","true"):i.addClass("active").attr("aria-pressed","true"),a.hasClass("file-zoom-fullscreen")?this._maximizeZoomDialog():e?this._maximizeZoomDialog():i.removeClass("active").attr("aria-pressed","false")))},_listen:function(){var i=this,a=i.$element,r=i.$form,n=i.$container;i._handler(a,"click",function(e){a.hasClass("file-no-browse")&&(a.data("zoneClicked")?a.data("zoneClicked",!1):e.preventDefault())}),i._handler(a,"change",e.proxy(i._change,i)),i.showBrowse&&i._handler(i.$btnFile,"click",e.proxy(i._browse,i)),i._handler(n.find(".fileinput-remove:not([disabled])"),"click",e.proxy(i.clear,i)),i._handler(n.find(".fileinput-cancel"),"click",e.proxy(i.cancel,i)),i._initDragDrop(),i._handler(r,"reset",e.proxy(i.clear,i)),i.isAjaxUpload||i._handler(r,"submit",e.proxy(i._submitForm,i)),i._handler(i.$container.find(".fileinput-upload"),"click",e.proxy(i._uploadClick,i)),i._handler(e(window),"resize",function(){i._listenFullScreen(screen.width===window.innerWidth&&screen.height===window.innerHeight)}),i._handler(e(document),"webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange",function(){i._listenFullScreen(t.checkFullScreen())}),i._autoFitContent(),i._initClickable(),i._refreshPreview()},_autoFitContent:function(){var t,i=window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,a=this,r=i<400?a.previewSettingsSmall||a.defaults.previewSettingsSmall:a.previewSettings||a.defaults.previewSettings;e.each(r,function(e,i){t=".file-preview-frame .file-preview-"+e,a.$preview.find(t+".kv-preview-data,"+t+" .kv-preview-data").css(i)})},_scanDroppedItems:function(e,t,i){i=i||"";var a,r,n,s=this,o=function(e){s._log("Error scanning dropped files!"),s._log(e)};e.isFile?e.file(function(e){t.push(e)},o):e.isDirectory&&(r=e.createReader(),(n=function(){r.readEntries(function(r){if(r&&r.length>0){for(a=0;a-1;if(this._zoneDragDropInit(i),this.isDisabled||!a)return i.originalEvent.dataTransfer.effectAllowed="none",void(i.originalEvent.dataTransfer.dropEffect="none");t.addCss(this.$dropZone,"file-highlighted")},_zoneDragLeave:function(e){this._zoneDragDropInit(e),this.isDisabled||this.$dropZone.removeClass("file-highlighted")},_zoneDrop:function(e){var i,a=this,r=a.$element,n=e.originalEvent.dataTransfer,s=n.files,o=n.items,l=t.getDragDropFolders(o),d=function(){a.isAjaxUpload?a._change(e,s):(a.changeTriggered=!0,r.get(0).files=s,setTimeout(function(){a.changeTriggered=!1,r.trigger("change"+a.namespace)},10)),a.$dropZone.removeClass("file-highlighted")};if(e.preventDefault(),!a.isDisabled&&!t.isEmpty(s))if(l>0){if(!a.isAjaxUpload)return void a._showFolderError(l);for(s=[],i=0;i"+t+""},_getModalContent:function(){return this._getLayoutTemplate("modal").setTokens({rtl:this.rtl?" kv-rtl":"",zoomFrameClass:this.frameClass,heading:this.msgZoomModalHeading,prev:this._getZoomButton("prev"),next:this._getZoomButton("next"),toggleheader:this._getZoomButton("toggleheader"),fullscreen:this._getZoomButton("fullscreen"),borderless:this._getZoomButton("borderless"),close:this._getZoomButton("close")})},_listenModalEvent:function(e){var i=this,a=i.$modal;a.on(e+".bs.modal",function(r){var n=a.find(".btn-fullscreen"),s=a.find(".btn-borderless");i._raise("filezoom"+e,function(e){return{sourceEvent:e,previewId:a.data("previewId"),modal:a}}(r)),"shown"===e&&(s.removeClass("active").attr("aria-pressed","false"),n.removeClass("active").attr("aria-pressed","false"),a.hasClass("file-zoom-fullscreen")&&(i._maximizeZoomDialog(),t.checkFullScreen()?n.addClass("active").attr("aria-pressed","true"):s.addClass("active").attr("aria-pressed","true")))})},_initZoom:function(){var i,a=this,r=a._getLayoutTemplate("modalMain"),n="#"+t.MODAL_ID;a.showPreview&&(a.$modal=e(n),a.$modal&&a.$modal.length||(i=e(document.createElement("div")).html(r).insertAfter(a.$container),a.$modal=e(n).insertBefore(i),i.remove()),t.initModal(a.$modal),a.$modal.html(a._getModalContent()),e.each(t.MODAL_EVENTS,function(e,t){a._listenModalEvent(t)}))},_initZoomButtons:function(){var t,i,a=this.$modal.data("previewId")||"",r=this.getFrames().toArray(),n=r.length,s=this.$modal.find(".btn-prev"),o=this.$modal.find(".btn-next");if(r.length<2)return s.hide(),void o.hide();s.show(),o.show(),n&&(t=e(r[0]),i=e(r[n-1]),s.removeAttr("disabled"),o.removeAttr("disabled"),t.length&&t.attr("id")===a&&s.attr("disabled",!0),i.length&&i.attr("id")===a&&o.attr("disabled",!0))},_maximizeZoomDialog:function(){var t=this.$modal,i=t.find(".modal-header:visible"),a=t.find(".modal-footer:visible"),r=t.find(".modal-body"),n=e(window).height();t.addClass("file-zoom-fullscreen"),i&&i.length&&(n-=i.outerHeight(!0)),a&&a.length&&(n-=a.outerHeight(!0)),r&&r.length&&(n-=r.outerHeight(!0)-r.height()),t.find(".kv-zoom-body").height(n)},_resizeZoomDialog:function(e){var i=this.$modal,a=i.find(".btn-fullscreen"),r=i.find(".btn-borderless");if(i.hasClass("file-zoom-fullscreen"))t.toggleFullScreen(!1),e?a.hasClass("active")||(i.removeClass("file-zoom-fullscreen"),this._resizeZoomDialog(!0),r.hasClass("active")&&r.removeClass("active").attr("aria-pressed","false")):a.hasClass("active")?a.removeClass("active").attr("aria-pressed","false"):(i.removeClass("file-zoom-fullscreen"),this.$modal.find(".kv-zoom-body").css("height",this.zoomModalHeight));else{if(!e)return void this._maximizeZoomDialog();t.toggleFullScreen(!0)}i.focus()},_setZoomContent:function(i,a){var r,n,s,o,l,d,c,h,p=this,u=i.attr("id"),f=p.$modal,m=f.find(".btn-prev"),g=f.find(".btn-next"),v=f.find(".btn-fullscreen"),w=f.find(".btn-borderless"),_=f.find(".btn-toggleheader"),b=p.$preview.find("#zoom-"+u);n=b.attr("data-template")||"generic",s=(r=b.find(".kv-file-content")).length?r.html():"",o=(i.data("caption")||"")+" "+(i.data("size")||""),f.find(".kv-zoom-title").attr("title",e("
").html(o).text()).html(o),l=f.find(".kv-zoom-body"),f.removeClass("kv-single-content"),a?(h=l.addClass("file-thumb-loading").clone().insertAfter(l),l.html(s).hide(),h.fadeOut("fast",function(){l.fadeIn("fast",function(){l.removeClass("file-thumb-loading")}),h.remove()})):l.html(s),(c=p.previewZoomSettings[n])&&(d=l.find(".kv-preview-data"),t.addCss(d,"file-zoom-detail"),e.each(c,function(e,t){d.css(e,t),(d.attr("width")&&"width"===e||d.attr("height")&&"height"===e)&&d.removeAttr(e)})),f.data("previewId",u),p._handler(m,"click",function(){p._zoomSlideShow("prev",u)}),p._handler(g,"click",function(){p._zoomSlideShow("next",u)}),p._handler(v,"click",function(){p._resizeZoomDialog(!0)}),p._handler(w,"click",function(){p._resizeZoomDialog(!1)}),p._handler(_,"click",function(){var e,t=f.find(".modal-header"),i=f.find(".modal-body .floating-buttons"),a=t.find(".kv-zoom-actions"),r=function(e){var i=p.$modal.find(".kv-zoom-body"),a=p.zoomModalHeight;f.hasClass("file-zoom-fullscreen")&&(a=i.outerHeight(!0),e||(a-=t.outerHeight(!0))),i.css("height",e?a+e:a)};t.is(":visible")?(e=t.outerHeight(!0),t.slideUp("slow",function(){a.find(".btn").appendTo(i),r(e)})):(i.find(".btn").appendTo(a),t.slideDown("slow",function(){r()})),f.focus()}),p._handler(f,"keydown",function(e){var t=e.which||e.keyCode;37!==t||m.attr("disabled")||p._zoomSlideShow("prev",u),39!==t||g.attr("disabled")||p._zoomSlideShow("next",u)})},_zoomPreview:function(e){var i,a=this.$modal;if(!e.length)throw"Cannot zoom to detailed preview!";t.initModal(a),a.html(this._getModalContent()),i=e.closest(t.FRAMES),this._setZoomContent(i),a.modal("show"),this._initZoomButtons()},_zoomSlideShow:function(t,i){var a,r,n,s=this.$modal.find(".kv-zoom-actions .btn-"+t),o=this.getFrames().toArray(),l=o.length;if(!s.attr("disabled")){for(r=0;r=l||!o[n]||((a=e(o[n])).length&&this._setZoomContent(a,!0),this._initZoomButtons(),this._raise("filezoom"+t,{previewId:i,modal:this.$modal}))}},_initZoomButton:function(){var t=this;t.$preview.find(".kv-file-zoom").each(function(){var i=e(this);t._handler(i,"click",function(){t._zoomPreview(i)})})},_inputFileCount:function(){return this.$element.get(0).files.length},_refreshPreview:function(){var e;this._inputFileCount()&&this.showPreview&&this.isPreviewable&&(this.isAjaxUpload?(e=this.getFileStack(),this.filestack=[],e.length?this._clearFileInput():e=this.$element.get(0).files):e=this.$element.get(0).files,e&&e.length&&(this.readFiles(e),this._setFileDropZoneTitle()))},_clearObjects:function(t){t.find("video audio").each(function(){this.pause(),e(this).remove()}),t.find("img object div").each(function(){e(this).remove()})},_clearFileInput:function(){var t,i,a,r=this.$element;this._inputFileCount()&&(t=r.closest("form"),i=e(document.createElement("form")),a=e(document.createElement("div")),r.before(a),t.length?t.after(i):a.after(i),i.append(r).trigger("reset"),a.before(r).remove(),i.remove())},_resetUpload:function(){this.uploadCache={content:[],config:[],tags:[],append:!0},this.uploadCount=0,this.uploadStatus={},this.uploadLog=[],this.uploadAsyncCount=0,this.loadedImages=[],this.totalImagesCount=0,this.$btnUpload.removeAttr("disabled"),this._setProgress(0),this.$progress.hide(),this._resetErrors(!1),this.ajaxAborted=!1,this.ajaxRequests=[],this._resetCanvas(),this.cacheInitialPreview={},this.overwriteInitial&&(this.initialPreview=[],this.initialPreviewConfig=[],this.initialPreviewThumbTags=[],this.previewCache.data={content:[],config:[],tags:[]})},_resetCanvas:function(){this.canvas&&this.imageCanvasContext&&this.imageCanvasContext.clearRect(0,0,this.canvas.width,this.canvas.height)},_hasInitialPreview:function(){return!this.overwriteInitial&&this.previewCache.count()},_resetPreview:function(){var e,t;this.previewCache.count()?(e=this.previewCache.out(),this._setPreviewContent(e.content),this._setInitThumbAttr(),t=this.initialCaption?this.initialCaption:e.caption,this._setCaption(t)):(this._clearPreview(),this._initCaption()),this.showPreview&&(this._initZoom(),this._initSortable())},_clearDefaultPreview:function(){this.$preview.find(".file-default-preview").remove()},_validateDefaultPreview:function(){this.showPreview&&!t.isEmpty(this.defaultPreviewContent)&&(this._setPreviewContent(''+this.defaultPreviewContent+"
"),this.$container.removeClass("file-input-new"),this._initClickable())},_resetPreviewThumbs:function(e){var t;if(e)return this._clearPreview(),void this.clearStack();this._hasInitialPreview()?(t=this.previewCache.out(),this._setPreviewContent(t.content),this._setInitThumbAttr(),this._setCaption(t.caption),this._initPreviewActions()):this._clearPreview()},_getLayoutTemplate:function(e){var i=this.layoutTemplates[e];return t.isEmpty(this.customLayoutTags)?i:t.replaceTags(i,this.customLayoutTags)},_getPreviewTemplate:function(e){var i=this.previewTemplates[e];return t.isEmpty(this.customPreviewTags)?i:t.replaceTags(i,this.customPreviewTags)},_getOutData:function(e,t,i){return e=e||{},t=t||{},i=i||this.filestack.slice(0)||{},{form:this.formdata,files:i,filenames:this.filenames,filescount:this.getFilesCount(),extra:this._getExtraData(),response:t,reader:this.reader,jqXHR:e}},_getMsgSelected:function(e){var t=1===e?this.fileSingle:this.filePlural;return e>0?this.msgSelected.replace("{n}",e).replace("{files}",t):this.msgNoFilesSelected},_getFrame:function(t){var i=e("#"+t);return i.length?i:(this._log('Invalid thumb frame with id: "'+t+'".'),null)},_getThumbs:function(e){return e=e||"",this.getFrames(":not(.file-preview-initial)"+e)},_getExtraData:function(e,t){var i=this.uploadExtraData;return"function"==typeof this.uploadExtraData&&(i=this.uploadExtraData(e,t)),i},_initXhr:function(e,t,i){var a=this;return e.upload&&e.upload.addEventListener("progress",function(e){var r=0,n=e.total,s=e.loaded||e.position;e.lengthComputable&&(r=Math.floor(s/n*100)),t?a._setAsyncUploadStatus(t,r,i):a._setProgress(r)},!1),e},_initAjaxSettings:function(){this._ajaxSettings=e.extend(!0,{},this.ajaxSettings),this._ajaxDeleteSettings=e.extend(!0,{},this.ajaxDeleteSettings)},_mergeAjaxCallback:function(e,t,i){var a,r=this._ajaxSettings,n=this.mergeAjaxCallbacks;"delete"===i&&(r=this._ajaxDeleteSettings,n=this.mergeAjaxDeleteCallbacks),a=r[e],r[e]=n&&"function"==typeof a?"before"===n?function(){a.apply(this,arguments),t.apply(this,arguments)}:function(){t.apply(this,arguments),a.apply(this,arguments)}:t},_ajaxSubmit:function(t,i,a,r,n,s){var o,l=this;l._raise("filepreajax",[n,s])&&(l._uploadExtra(n,s),l._initAjaxSettings(),l._mergeAjaxCallback("beforeSend",t),l._mergeAjaxCallback("success",i),l._mergeAjaxCallback("complete",a),l._mergeAjaxCallback("error",r),o=e.extend(!0,{},{xhr:function(){var t=e.ajaxSettings.xhr();return l._initXhr(t,n,l.getFileStack().length)},url:s&&l.uploadUrlThumb?l.uploadUrlThumb:l.uploadUrl,type:"POST",dataType:"json",data:l.formdata,cache:!1,processData:!1,contentType:!1},l._ajaxSettings),l.ajaxRequests.push(e.ajax(o)))},_mergeArray:function(e,i){var a=t.cleanArray(this[e]),r=t.cleanArray(i);this[e]=a.concat(r)},_initUploadSuccess:function(i,a,r){var n,s,o,l,d,c,h,p,u,f=this;f.showPreview&&"object"==typeof i&&!e.isEmptyObject(i)&&void 0!==i.initialPreview&&i.initialPreview.length>0&&(f.hasInitData=!0,c=i.initialPreview||[],h=i.initialPreviewConfig||[],p=i.initialPreviewThumbTags||[],n=void 0===i.append||i.append,c.length>0&&!t.isArray(c)&&(c=c.split(f.initialPreviewDelimiter)),f._mergeArray("initialPreview",c),f._mergeArray("initialPreviewConfig",h),f._mergeArray("initialPreviewThumbTags",p),void 0!==a?r?(u=a.attr("data-fileindex"),f.uploadCache.content[u]=c[0],f.uploadCache.config[u]=h[0]||[],f.uploadCache.tags[u]=p[0]||[],f.uploadCache.append=n):(o=f.previewCache.add(c,h[0],p[0],n),s=f.previewCache.get(o,!1),(d=(l=e(document.createElement("div")).html(s).hide().insertAfter(a)).find(".kv-zoom-cache"))&&d.length&&d.insertAfter(a),a.fadeOut("slow",function(){var e=l.find(".file-preview-frame");e&&e.length&&e.insertBefore(a).fadeIn("slow").css("display:inline-block"),f._initPreviewActions(),f._clearFileInput(),t.cleanZoomCache(f.$preview.find("#zoom-"+a.attr("id"))),a.remove(),l.remove(),f._initSortable()})):(f.previewCache.set(c,h,p,n),f._initPreview(),f._initPreviewActions()))},_initSuccessThumbs:function(){var i=this;i.showPreview&&i._getThumbs(t.FRAMES+".file-preview-success").each(function(){var a=e(this),r=i.$preview,n=a.find(".kv-file-remove");n.removeAttr("disabled"),i._handler(n,"click",function(){var e=a.attr("id"),n=i._raise("filesuccessremove",[e,a.attr("data-fileindex")]);t.cleanMemory(a),!1!==n&&a.fadeOut("slow",function(){t.cleanZoomCache(r.find("#zoom-"+e)),a.remove(),i.getFrames().length||i.reset()})})})},_checkAsyncComplete:function(){var t,i;for(i=0;i0||!e.isEmptyObject(m.uploadExtraData),b=e("#"+w).find(".file-thumb-progress"),C={id:w,index:i};m.formdata=v,m.showPreview&&(n=e("#"+w+":not(.file-preview-initial)"),o=n.find(".kv-file-upload"),l=n.find(".kv-file-remove"),b.show()),0===g||!_||o&&o.hasClass("disabled")||m._abort(C)||(f=function(e,t){d||m.updateStack(e,void 0),m.uploadLog.push(t),m._checkAsyncComplete()&&(m.fileBatchCompleted=!0)},s=function(){var e,i,a,r=m.uploadCache,n=0,s=m.cacheInitialPreview;m.fileBatchCompleted&&(s&&s.content&&(n=s.content.length),setTimeout(function(){var o=0===m.getFileStack(!0).length;if(m.showPreview){if(m.previewCache.set(r.content,r.config,r.tags,r.append),n){for(i=0;i0||!e.isEmptyObject(o.uploadExtraData);o.formdata=new FormData,0!==d&&c&&!o._abort({})&&(s=function(){e.each(l,function(e){o.updateStack(e,void 0)}),o._clearFileInput()},i=function(i){o.lock();var a=o._getOutData(i);o.ajaxAborted=!1,o.showPreview&&o._getThumbs().each(function(){var i=e(this),a=i.find(".kv-file-upload"),r=i.find(".kv-file-remove");i.hasClass("file-preview-success")||(o._setThumbStatus(i,"Loading"),t.addCss(i,"file-uploading")),a.attr("disabled",!0),r.attr("disabled",!0)}),o._raise("filebatchpreupload",[a]),o._abort(a)&&(i.abort(),o._getThumbs().each(function(){var t=e(this),i=t.find(".kv-file-upload"),a=t.find(".kv-file-remove");t.hasClass("file-preview-loading")&&(o._setThumbStatus(t,"New"),t.removeClass("file-uploading")),i.removeAttr("disabled"),a.removeAttr("disabled")}),o._setProgressCancelled())},a=function(i,a,r){var n=o._getOutData(r,i),l=0,d=o._getThumbs(":not(.file-preview-success)"),c=t.isEmpty(i)||t.isEmpty(i.errorkeys)?[]:i.errorkeys;t.isEmpty(i)||t.isEmpty(i.error)?(o._raise("filebatchuploadsuccess",[n]),s(),o.showPreview?(d.each(function(){var t=e(this);o._setThumbStatus(t,"Success"),t.removeClass("file-uploading"),t.find(".kv-file-upload").hide().removeAttr("disabled")}),o._initUploadSuccess(i)):o.reset(),o._setProgress(101)):(o.showPreview&&(d.each(function(){var t=e(this),i=t.attr("data-fileindex");t.removeClass("file-uploading"),t.find(".kv-file-upload").removeAttr("disabled"),t.find(".kv-file-remove").removeAttr("disabled"),0===c.length||-1!==e.inArray(l,c)?(o._setPreviewError(t,i,o.filestack[i],o.retryErrorUploads),o.retryErrorUploads||(t.find(".kv-file-upload").hide(),o.updateStack(i,void 0))):(t.find(".kv-file-upload").hide(),o._setThumbStatus(t,"Success"),o.updateStack(i,void 0)),t.hasClass("file-preview-error")&&!o.retryErrorUploads||l++}),o._initUploadSuccess(i)),o._showUploadError(i.error,n,"filebatchuploaderror"),o._setProgress(101,o.$progress,o.msgUploadError))},n=function(){o.unlock(),o._initSuccessThumbs(),o._clearFileInput(),o._raise("filebatchuploadcomplete",[o.filestack,o._getExtraData()])},r=function(t,i,a){var r=o._getOutData(t),n=o.ajaxOperations.uploadBatch,s=o._parseError(n,t,a);o._showUploadError(s,r,"filebatchuploaderror"),o.uploadFileCount=d-1,o.showPreview&&(o._getThumbs().each(function(){var t=e(this),i=t.attr("data-fileindex");t.removeClass("file-uploading"),void 0!==o.filestack[i]&&o._setPreviewError(t)}),o._getThumbs().removeClass("file-uploading"),o._getThumbs(" .kv-file-upload").removeAttr("disabled"),o._getThumbs(" .kv-file-delete").removeAttr("disabled"),o._setProgress(101,o.$progress,o.msgAjaxProgressError.replace("{operation}",n)))},e.each(l,function(e,i){t.isEmpty(l[e])||o.formdata.append(o.uploadFileAttr,i,o.filenames[e])}),o._ajaxSubmit(i,a,n,r))},_uploadExtraOnly:function(){var e,i,a,r,n=this,s={};n.formdata=new FormData,n._abort(s)||(e=function(e){n.lock();var t=n._getOutData(e);n._raise("filebatchpreupload",[t]),n._setProgress(50),s.data=t,s.xhr=e,n._abort(s)&&(e.abort(),n._setProgressCancelled())},i=function(e,i,a){var r=n._getOutData(a,e);t.isEmpty(e)||t.isEmpty(e.error)?(n._raise("filebatchuploadsuccess",[r]),n._clearFileInput(),n._initUploadSuccess(e),n._setProgress(101)):n._showUploadError(e.error,r,"filebatchuploaderror")},a=function(){n.unlock(),n._clearFileInput(),n._raise("filebatchuploadcomplete",[n.filestack,n._getExtraData()])},r=function(e,t,i){var a=n._getOutData(e),r=n.ajaxOperations.uploadExtra,o=n._parseError(r,e,i);s.data=a,n._showUploadError(o,a,"filebatchuploaderror"),n._setProgress(101,n.$progress,n.msgAjaxProgressError.replace("{operation}",r))},n._ajaxSubmit(e,i,a,r))},_deleteFileIndex:function(i){var a=i.attr("data-fileindex"),r=this.reversePreviewOrder;"init_"===a.substring(0,5)&&(a=parseInt(a.replace("init_","")),this.initialPreview=t.spliceArray(this.initialPreview,a,r),this.initialPreviewConfig=t.spliceArray(this.initialPreviewConfig,a,r),this.initialPreviewThumbTags=t.spliceArray(this.initialPreviewThumbTags,a,r),this.getFrames().each(function(){var t=e(this),i=t.attr("data-fileindex");"init_"===i.substring(0,5)&&(i=parseInt(i.replace("init_","")))>a&&(i--,t.attr("data-fileindex","init_"+i))}),this.uploadAsync&&(this.cacheInitialPreview=this.getPreview()))},_initFileActions:function(){var i=this,a=i.$preview;i.showPreview&&(i._initZoomButton(),i.getFrames(" .kv-file-remove").each(function(){var r,n,s,o=e(this),l=o.closest(t.FRAMES),d=l.attr("id"),c=l.attr("data-fileindex");i._handler(o,"click",function(){if(!1===i._raise("filepreremove",[d,c])||!i._validateMinCount())return!1;r=l.hasClass("file-preview-error"),t.cleanMemory(l),l.fadeOut("slow",function(){t.cleanZoomCache(a.find("#zoom-"+d)),i.updateStack(c,void 0),i._clearObjects(l),l.remove(),d&&r&&i.$errorContainer.find('li[data-file-id="'+d+'"]').fadeOut("fast",function(){e(this).remove(),i._errorsExist()||i._resetErrors()}),i._clearFileInput();var o=i.getFileStack(!0),h=i.previewCache.count(),p=o.length,u=i.showPreview&&i.getFrames().length;0!==p||0!==h||u?(s=(n=h+p)>1?i._getMsgSelected(n):o[0]?i._getFileNames()[0]:"",i._setCaption(s)):i.reset(),i._raise("fileremoved",[d,c])})})}),i.getFrames(" .kv-file-upload").each(function(){var a=e(this);i._handler(a,"click",function(){var e=a.closest(t.FRAMES),r=e.attr("data-fileindex");i.$progress.hide(),e.hasClass("file-preview-error")&&!i.retryErrorUploads||i._uploadSingle(r,!1)})}))},_initPreviewActions:function(){var i=this,a=i.$preview,r=i.deleteExtraData||{},n=t.FRAMES+" .kv-file-remove",s=i.fileActionSettings,o=s.removeClass,l=s.removeErrorClass,d=function(){var e=i.isAjaxUpload?i.previewCache.count():i._inputFileCount();a.find(t.FRAMES).length||e||(i._setCaption(""),i.reset(),i.initialCaption="")};i._initZoomButton(),a.find(n).each(function(){var n,s,c,h=e(this),p=h.data("url")||i.deleteUrl,u=h.data("key");if(!t.isEmpty(p)&&void 0!==u){var f,m,g,v,w=h.closest(t.FRAMES),_=i.previewCache.data,b=w.attr("data-fileindex");b=parseInt(b.replace("init_","")),g=t.isEmpty(_.config)&&t.isEmpty(_.config[b])?null:_.config[b],"function"==typeof(v=t.isEmpty(g)||t.isEmpty(g.extra)?r:g.extra)&&(v=v()),m={id:h.attr("id"),key:u,extra:v},n=function(e){i.ajaxAborted=!1,i._raise("filepredelete",[u,e,v]),i._abort()?e.abort():(h.removeClass(l),t.addCss(w,"file-uploading"),t.addCss(h,"disabled "+o))},s=function(e,r,n){var s,c;if(!t.isEmpty(e)&&!t.isEmpty(e.error))return m.jqXHR=n,m.response=e,i._showError(e.error,m,"filedeleteerror"),w.removeClass("file-uploading"),h.removeClass("disabled "+o).addClass(l),void d();w.removeClass("file-uploading").addClass("file-deleted"),w.fadeOut("slow",function(){b=parseInt(w.attr("data-fileindex").replace("init_","")),i.previewCache.unset(b),i._deleteFileIndex(w),s=i.previewCache.count(),c=s>0?i._getMsgSelected(s):"",i._setCaption(c),i._raise("filedeleted",[u,n,v]),t.cleanZoomCache(a.find("#zoom-"+w.attr("id"))),i._clearObjects(w),w.remove(),d()})},c=function(e,t,a){var r=i.ajaxOperations.deleteThumb,n=i._parseError(r,e,a);m.jqXHR=e,m.response={},i._showError(n,m,"filedeleteerror"),w.removeClass("file-uploading"),h.removeClass("disabled "+o).addClass(l),d()},i._initAjaxSettings(),i._mergeAjaxCallback("beforeSend",n,"delete"),i._mergeAjaxCallback("success",s,"delete"),i._mergeAjaxCallback("error",c,"delete"),f=e.extend(!0,{},{url:p,type:"POST",dataType:"json",data:e.extend(!0,{},{key:u},v)},i._ajaxDeleteSettings),i._handler(h,"click",function(){if(!i._validateMinCount())return!1;i.ajaxAborted=!1,i._raise("filebeforedelete",[u,v]),i.ajaxAborted instanceof Promise?i.ajaxAborted.then(function(t){t||e.ajax(f)}):i.ajaxAborted||e.ajax(f)})}})},_hideFileIcon:function(){this.overwriteInitial&&this.$captionContainer.removeClass("icon-visible")},_showFileIcon:function(){t.addCss(this.$captionContainer,"icon-visible")},_getSize:function(t){var i,a,r,n=parseFloat(t),s=this.fileSizeGetter;return e.isNumeric(t)&&e.isNumeric(n)?("function"==typeof s?r=s(n):0===n?r="0.00 B":(i=Math.floor(Math.log(n)/Math.log(1024)),a=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],r=1*(n/Math.pow(1024,i)).toFixed(2)+" "+a[i]),this._getLayoutTemplate("size").replace("{sizeText}",r)):""},_generatePreviewTemplate:function(i,a,r,n,s,o,l,d,c,h,p){var u,f=this,m=f.slug(r),g="",v="",w=(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)<400?f.previewSettingsSmall[i]||f.defaults.previewSettingsSmall[i]:f.previewSettings[i]||f.defaults.previewSettings[i],_=c||f._renderFileFooter(m,l,"auto",o),b=f._getPreviewIcon(r),C="type-default",y=b&&f.preferIconicPreview,x=b&&f.preferIconicZoomPreview;return w&&e.each(w,function(e,t){v+=e+":"+t+";"}),u=function(a,o,l,c){var u=l?"zoom-"+s:s,g=f._getPreviewTemplate(a),w=(d||"")+" "+c;return f.frameClass&&(w=f.frameClass+" "+w),l&&(w=w.replace(" "+t.SORT_CSS,"")),g=f._parseFilePreviewIcon(g,r),"text"===a&&(o=t.htmlEncode(o)),"object"!==i||n||e.each(f.defaults.fileTypeSettings,function(e,t){"object"!==e&&"other"!==e&&t(r,n)&&(C="type-"+e)}),g.setTokens({previewId:u,caption:m,frameClass:w,type:n,fileindex:h,typeCss:C,footer:_,data:o,template:p||i,style:v?'style="'+v+'"':""})},h=h||s.slice(s.lastIndexOf("-")+1),f.fileActionSettings.showZoom&&(g=u(x?"other":i,a,!0,"kv-zoom-thumb")),g="\n"+f._getLayoutTemplate("zoomCache").replace("{zoomContent}",g),u(y?"other":i,a,!1,"kv-preview-thumb")+g},_addToPreview:function(e,t){return this.reversePreviewOrder?e.prepend(t):e.append(t)},_previewDefault:function(i,a,r){var n=this.$preview;if(this.showPreview){var s,o=i?i.name:"",l=i?i.type:"",d=i.size||0,c=this.slug(o),h=!0===r&&!this.isAjaxUpload,p=t.objUrl.createObjectURL(i);this._clearDefaultPreview(),s=this._generatePreviewTemplate("other",p,o,l,a,h,d),this._addToPreview(n,s),this._setThumbAttr(a,c,d),!0===r&&this.isAjaxUpload&&this._setThumbStatus(e("#"+a),"Error")}},_previewFile:function(e,t,i,a,r,n){if(this.showPreview){var s,o=t?t.name:"",l=n.type,d=n.name,c=this._parseFileType(l,o),h=this.allowedPreviewTypes,p=this.allowedPreviewMimeTypes,u=this.$preview,f=t.size||0,m=h&&h.indexOf(c)>=0,g=p&&-1!==p.indexOf(l),v="text"===c||"html"===c||"image"===c?i.target.result:r;if("html"===c&&this.purifyHtml&&window.DOMPurify&&(v=window.DOMPurify.sanitize(v)),m||g){s=this._generatePreviewTemplate(c,v,o,l,a,!1,f),this._clearDefaultPreview(),this._addToPreview(u,s);var w=u.find("#"+a+" img");this._validateImageOrientation(w,t,a,d,l,f,v)}else this._previewDefault(t,a);this._setThumbAttr(a,d,f),this._initSortable()}},_setThumbAttr:function(t,i,a){var r=e("#"+t);r.length&&(a=a&&a>0?this._getSize(a):"",r.data({caption:i,size:a}))},_setInitThumbAttr:function(){var e,i,a,r,n=this.previewCache.data,s=this.previewCache.count();if(0!==s)for(var o=0;o&"']/g,"_")},_updateFileDetails:function(e){var i=this.$element,a=this.getFileStack(),r=t.isIE(9)&&t.findFileName(i.val())||i[0].files[0]&&i[0].files[0].name||a.length&&a[0].name||"",n=this.slug(r),s=this.isAjaxUpload?a.length:e,o=this.previewCache.count()+s,l=1===s?n:this._getMsgSelected(o);this.isError?(this.$previewContainer.removeClass("file-thumb-loading"),this.$previewStatus.html(""),this.$captionContainer.removeClass("icon-visible")):this._showFileIcon(),this._setCaption(l,this.isError),this.$container.removeClass("file-input-new file-input-ajax-new"),1===arguments.length&&this._raise("fileselect",[e,n]),this.previewCache.count()&&this._initPreviewActions()},_setThumbStatus:function(e,t){if(this.showPreview){var i="indicator"+t,a=i+"Title",r="file-preview-"+t.toLowerCase(),n=e.find(".file-upload-indicator"),s=this.fileActionSettings;e.removeClass("file-preview-success file-preview-error file-preview-loading"),"Success"===t&&e.find(".file-drag-handle").remove(),n.html(s[i]),n.attr("title",s[a]),e.addClass(r),"Error"!==t||this.retryErrorUploads||e.find(".kv-file-upload").attr("disabled",!0)}},_setProgressCancelled:function(){this._setProgress(101,this.$progress,this.msgCancelled)},_setProgress:function(e,i,a){var r,n=Math.min(e,100),s=this.progressUploadThreshold,o=e<=100?this.progressTemplate:this.progressCompleteTemplate,l=n<100?this.progressTemplate:a?this.progressErrorTemplate:o;i=i||this.$progress,t.isEmpty(l)||(r=s&&n>s&&e<=100?l.setTokens({percent:s,status:this.msgUploadThreshold}):l.setTokens({percent:n,status:e>100?this.msgUploadEnd:n+"%"}),i.html(r),a&&i.find('[role="progressbar"]').html(a))},_setFileDropZoneTitle:function(){var e,i=this.$container.find(".file-drop-zone"),a=this.dropZoneTitle;this.isClickable&&(e=t.isEmpty(this.$element.attr("multiple"))?this.fileSingle:this.filePlural,a+=this.dropZoneClickTitle.replace("{files}",e)),i.find("."+this.dropZoneTitleClass).remove(),!this.showPreview||0===i.length||this.getFileStack().length>0||!this.dropZoneEnabled||!this.isAjaxUpload&&this.$element.files||(0===i.find(t.FRAMES).length&&t.isEmpty(this.defaultPreviewContent)&&i.prepend(' '+a+"
"),this.$container.removeClass("file-input-new"),t.addCss(this.$container,"file-input-ajax-new"))},_setAsyncUploadStatus:function(t,i,a){var r=0;this._setProgress(i,e("#"+t).find(".file-thumb-progress")),this.uploadStatus[t]=i,e.each(this.uploadStatus,function(e,t){r+=t}),this._setProgress(Math.floor(r/a))},_validateMinCount:function(){var e=this.isAjaxUpload?this.getFileStack().length:this._inputFileCount();return!(this.validateInitialCount&&this.minFileCount>0&&this._getFileCount(e-1)=h:d<=h)||(l=this["msgImage"+s+i].setTokens({name:n,size:h}),this._showUploadError(l,o),this._setPreviewError(r,e,null)))},_getExifObj:function(e){var t=null;try{t=window.piexif?window.piexif.load(e):null}catch(e){t=null}return t||this._log("Error loading the piexif.js library."),t},_validateImageOrientation:function(e,i,a,r,n,s,o){var l,d;(d=(l=e.length&&this.autoOrientImage?this._getExifObj(o):null)?l["0th"][piexif.ImageIFD.Orientation]:null)?(t.setImageOrientation(e,this.$preview.find("#zoom-"+a+" img"),d),this._raise("fileimageoriented",{$img:e,file:i}),this._validateImage(a,r,n,s,o,l)):this._validateImage(a,r,n,s,o,l)},_validateImage:function(t,i,a,r,n,s){var o,l,d,c=this,h=c.$preview,p=h.find("#"+t),u=p.attr("data-fileindex"),f=p.find("img");i=i||"Untitled",f.one("load",function(){l=p.width(),d=h.width(),l>d&&f.css("width","100%"),o={ind:u,id:t},c._checkDimensions(u,"Small",f,p,i,"Width",o),c._checkDimensions(u,"Small",f,p,i,"Height",o),c.resizeImage||(c._checkDimensions(u,"Large",f,p,i,"Width",o),c._checkDimensions(u,"Large",f,p,i,"Height",o)),c._raise("fileimageloaded",[t]),c.loadedImages.push({ind:u,img:f,thumb:p,pid:t,typ:a,siz:r,validated:!1,imgData:n,exifObj:s}),p.data("exif",s),c._validateAllImages()}).one("error",function(){c._raise("fileimageloaderror",[t])}).each(function(){this.complete?e(this).trigger("load"):this.error&&e(this).trigger("error")})},_validateAllImages:function(){var e,t,i,a={val:0},r=this.loadedImages.length,n=this.resizeIfSizeMoreThan;if(r===this.totalImagesCount&&(this._raise("fileimagesloaded"),this.resizeImage))for(e=0;e1e3*n&&this._getResizedImage(t,a,r),this.loadedImages[e].validated=!0)},_getResizedImage:function(i,a,r){var n,s,o,l,d,c,h=this,p=e(i.img)[0],u=p.naturalWidth,f=p.naturalHeight,m=1,g=h.maxImageWidth||u,v=h.maxImageHeight||f,w=!(!u||!f),_=h.imageCanvas,b=h.imageCanvasContext,C=i.typ,y=i.pid,x=i.ind,T=i.thumb,E=i.exifObj;if(d=function(e,t,i){h.isAjaxUpload?h._showUploadError(e,t,i):h._showError(e,t,i),h._setPreviewError(T,x)},h.filestack[x]&&w&&!(u<=g&&f<=v)||(w&&h.filestack[x]&&h._raise("fileimageresized",[y,x]),a.val++,a.val===r&&h._raise("fileimagesresized"),w)){C=C||h.resizeDefaultImageType,s=u>g,o=f>v,m="width"===h.resizePreference?s?g/u:o?v/f:1:o?v/f:s?g/u:1,h._resetCanvas(),u*=m,f*=m,_.width=u,_.height=f;try{b.drawImage(p,0,0,u,f),l=_.toDataURL(C,h.resizeQuality),E&&(c=window.piexif.dump(E),l=window.piexif.insert(c,l)),n=t.dataURI2Blob(l),h.filestack[x]=n,h._raise("fileimageresized",[y,x]),a.val++,a.val===r&&h._raise("fileimagesresized",[void 0,void 0]),n instanceof Blob||d(h.msgImageResizeError,{id:y,index:x},"fileimageresizeerror")}catch(e){a.val++,a.val===r&&h._raise("fileimagesresized",[void 0,void 0]),d(h.msgImageResizeException.replace("{errors}",e.message),{id:y,index:x},"fileimageresizeexception")}}else d(h.msgImageResizeError,{id:y,index:x},"fileimageresizeerror")},_initBrowse:function(e){var i=this.$element;this.showBrowse?this.$btnFile=e.find(".btn-file").append(i):(i.appendTo(e).attr("tabindex",-1),t.addCss(i,"file-no-browse"))},_initClickable:function(){var i,a,r=this;r.isClickable&&(i=r.$dropZone,r.isAjaxUpload||(a=r.$preview.find(".file-default-preview")).length&&(i=a),t.addCss(i,"clickable"),i.attr("tabindex",-1),r._handler(i,"click",function(t){var a=e(t.target);e(r.elErrorContainer+":visible").length||a.parents(".file-preview-thumbnails").length&&!a.parents(".file-default-preview").length||(r.$element.data("zoneClicked",!0).trigger("click"),i.blur())}))},_initCaption:function(){var e=this.initialCaption||"";return this.overwriteInitial||t.isEmpty(e)?(this.$caption.val(""),!1):(this._setCaption(e),!0)},_setCaption:function(i,a){var r,n,s,o,l,d=this.getFileStack();if(this.$caption.length){if(this.$captionContainer.removeClass("icon-visible"),a)r=e(""+this.msgValidationError+"
").text(),l=(o=d.length)?1===o&&d[0]?this._getFileNames()[0]:this._getMsgSelected(o):this._getMsgSelected(this.msgNo),n=t.isEmpty(i)?l:i,s=''+this.msgValidationErrorIcon+" ";else{if(t.isEmpty(i))return;n=r=e(""+i+"
").text(),s=this._getLayoutTemplate("fileIcon")}this.$captionContainer.addClass("icon-visible"),this.$caption.attr("title",r).val(n),this.$captionIcon.html(s)}},_createContainer:function(){var t={class:"file-input file-input-new"+(this.rtl?" kv-rtl":"")},i=e(document.createElement("div")).attr(t).html(this._renderMain());return i.insertBefore(this.$element),this._initBrowse(i),this.theme&&i.addClass("theme-"+this.theme),i},_refreshContainer:function(){var e=this.$container;this.$element.insertAfter(e),e.html(this._renderMain()),this._initBrowse(e),this._validateDisabled()},_validateDisabled:function(){this.$caption.attr({readonly:this.isDisabled})},_renderMain:function(){var e=this.dropZoneEnabled?" file-drop-zone":"file-drop-disabled",t=this.showClose?this._getLayoutTemplate("close"):"",i=this.showPreview?this._getLayoutTemplate("preview").setTokens({class:this.previewClass,dropClass:e}):"",a=this.isDisabled?this.captionClass+" file-caption-disabled":this.captionClass,r=this.captionTemplate.setTokens({class:a+" kv-fileinput-caption"});return this.mainTemplate.setTokens({class:this.mainClass+(!this.showBrowse&&this.showCaption?" no-browse":""),preview:i,close:t,caption:r,upload:this._renderButton("upload"),remove:this._renderButton("remove"),cancel:this._renderButton("cancel"),browse:this._renderButton("browse")})},_renderButton:function(e){var i=this._getLayoutTemplate("btnDefault"),a=this[e+"Class"],r=this[e+"Title"],n=this[e+"Icon"],s=this[e+"Label"],o=this.isDisabled?" disabled":"",l="button";switch(e){case"remove":if(!this.showRemove)return"";break;case"cancel":if(!this.showCancel)return"";a+=" kv-hidden";break;case"upload":if(!this.showUpload)return"";this.isAjaxUpload&&!this.isDisabled?i=this._getLayoutTemplate("btnLink").replace("{href}",this.uploadUrl):l="submit";break;case"browse":if(!this.showBrowse)return"";i=this._getLayoutTemplate("btnBrowse");break;default:return""}return a+="browse"===e?" btn-file":" fileinput-"+e+" fileinput-"+e+"-button",t.isEmpty(s)||(s=' '+s+" "),i.setTokens({type:l,css:a,title:r,status:o,icon:n,label:s})},_renderThumbProgress:function(){return''+this.progressTemplate.setTokens({percent:"0",status:this.msgUploadBegin})+"
"},_renderFileFooter:function(e,i,a,r){var n,s=this.fileActionSettings,o=s.showRemove,l=s.showDrag,d=s.showUpload,c=s.showZoom,h=this._getLayoutTemplate("footer"),p=this._getLayoutTemplate("indicator"),u=r?s.indicatorError:s.indicatorNew,f=r?s.indicatorErrorTitle:s.indicatorNewTitle,m=p.setTokens({indicator:u,indicatorTitle:f});return i=this._getSize(i),n=this.isAjaxUpload?h.setTokens({actions:this._renderFileActions(d,!1,o,c,l,!1,!1,!1),caption:e,size:i,width:a,progress:this._renderThumbProgress(),indicator:m}):h.setTokens({actions:this._renderFileActions(!1,!1,!1,c,l,!1,!1,!1),caption:e,size:i,width:a,progress:"",indicator:m}),n=t.replaceTags(n,this.previewThumbTags)},_renderFileActions:function(e,t,i,a,r,n,s,o,l,d,c){if(!(e||t||i||a||r))return"";var h,p=!1===s?"":' data-url="'+s+'"',u=!1===o?"":' data-key="'+o+'"',f="",m="",g="",v="",w="",_=this._getLayoutTemplate("actions"),b=this.fileActionSettings,C=this.otherActionButtons.setTokens({dataKey:u,key:o}),y=n?b.removeClass+" disabled":b.removeClass;return i&&(f=this._getLayoutTemplate("actionDelete").setTokens({removeClass:y,removeIcon:b.removeIcon,removeTitle:b.removeTitle,dataUrl:p,dataKey:u,key:o})),e&&(m=this._getLayoutTemplate("actionUpload").setTokens({uploadClass:b.uploadClass,uploadIcon:b.uploadIcon,uploadTitle:b.uploadTitle})),t&&(g=(g=this._getLayoutTemplate("actionDownload").setTokens({downloadClass:b.downloadClass,downloadIcon:b.downloadIcon,downloadTitle:b.downloadTitle,downloadUrl:d||this.initialPreviewDownloadUrl})).setTokens({filename:c,key:o})),a&&(v=this._getLayoutTemplate("actionZoom").setTokens({zoomClass:b.zoomClass,zoomIcon:b.zoomIcon,zoomTitle:b.zoomTitle})),r&&l&&(h="drag-handle-init "+b.dragClass,w=this._getLayoutTemplate("actionDrag").setTokens({dragClass:h,dragTitle:b.dragTitle,dragIcon:b.dragIcon})),_.setTokens({delete:f,upload:m,download:g,zoom:v,drag:w,other:C})},_browse:function(e){e&&e.isDefaultPrevented()||!this._raise("filebrowse")||(this.isError&&!this.isAjaxUpload&&this.clear(),this.$captionContainer.focus())},_filterDuplicate:function(e,t,i){var a=this._getFileId(e);a&&i&&i.indexOf(a)>-1||(i||(i=[]),t.push(e),i.push(a))},_change:function(i){var a=this;if(!a.changeTriggered){var r,n,s,o,l,d,c,h,p,u,f,m=a.$element,g=arguments.length>1,v=a.isAjaxUpload,w=[],_=g?arguments[1]:m.get(0).files,b=!v&&t.isEmpty(m.attr("multiple"))?1:a.maxFileCount,C=a.filestack.length,y=t.isEmpty(m.attr("multiple"))&&C>0,x=a._getFileIds();if(a.reader=null,a._resetUpload(),a._hideFileIcon(),a.dropZoneEnabled&&a.$container.find(".file-drop-zone ."+a.dropZoneTitleClass).remove(),v?e.each(_,function(e,t){a._filterDuplicate(t,w,x)}):(_=i.target&&void 0===i.target.files?i.target.value?[{name:i.target.value.replace(/^.+\\/,"")}]:[]:i.target.files||{},w=_),t.isEmpty(w)||0===w.length)return v||a.clear(),void a._raise("fileselectnone");if(a._resetErrors(),n=w.length,r=a._getFileCount(v?a.getFileStack().length+n:n),b>0&&r>b){if(!a.autoReplace||n>b)return s=a.autoReplace&&n>b?n:r,o=b,f=a.msgFilesTooMany.replace("{m}",o).replace("{n}",s),a.isError=(l=f,d=null,c=null,h=null,p=e.extend(!0,{},a._getOutData({},{},_),{id:c,index:h}),u={id:c,index:h,file:d,files:_},v?a._showUploadError(l,p):a._showError(l,u)),a.$captionContainer.removeClass("icon-visible"),a._setCaption("",!0),void a.$container.removeClass("file-input-new file-input-ajax-new");r>b&&a._resetPreviewThumbs(v)}else!v||y?(a._resetPreviewThumbs(!1),y&&a.clearStack()):!v||0!==C||a.previewCache.count()&&!a.overwriteInitial||a._resetPreviewThumbs(!0);a.isPreviewable?a.readFiles(w):a._updateFileDetails(1)}},_abort:function(t){var i;return this.ajaxAborted&&"object"==typeof this.ajaxAborted&&void 0!==this.ajaxAborted.message?((i=e.extend(!0,{},this._getOutData(),t)).abortData=this.ajaxAborted.data||{},i.abortMessage=this.ajaxAborted.message,this._setProgress(101,this.$progress,this.msgCancelled),this._showUploadError(this.ajaxAborted.message,i,"filecustomerror"),this.cancel(),!0):!!this.ajaxAborted},_resetFileStack:function(){var i=this,a=0,r=[],n=[],s=[];i._getThumbs().each(function(){var o=e(this),l=o.attr("data-fileindex"),d=i.filestack[l],c=o.attr("id");"-1"!==l&&-1!==l&&(void 0!==d?(r[a]=d,n[a]=i._getFileName(d),s[a]=i._getFileId(d),o.attr({id:i.previewInitId+"-"+a,"data-fileindex":a}),a++):o.attr({id:"uploaded-"+t.uniqId(),"data-fileindex":"-1"}),i.$preview.find("#zoom-"+c).attr({id:"zoom-"+o.attr("id"),"data-fileindex":o.attr("data-fileindex")}))}),i.filestack=r,i.filenames=n,i.fileids=s},_isFileSelectionValid:function(e){return e=e||0,this.required&&!this.getFilesCount()?(this.$errorContainer.html(""),this._showUploadError(this.msgFileRequired),!1):!(this.minFileCount>0&&this._getFileCount(e)=u)return r.isAjaxUpload&&r.filestack.length>0?r._raise("filebatchselected",[r.getFileStack()]):r._raise("filebatchselected",[i]),l.removeClass("file-thumb-loading"),void d.html("");var T,E,S,k,F,P,I,A,D,z,$,j,U=p+"-"+(m+x),B=i[x],R=f.text,O=f.image,L=f.html,M=B&&B.name?r.slug(B.name):"",Z=(B&&B.size||0)/1e3,N="",H=B?t.objUrl.createObjectURL(B):null,W=0,q="",V=0,K=function(){var e=h.setTokens({index:x+1,files:u,percent:50,name:M});setTimeout(function(){d.html(e),r._updateFileDetails(u),a(x+1)},100),r._raise("fileloaded",[B,U,x,o])};if(B){if(v>0)for(E=0;E0&&Z>r.maxFileSize)return S=r.msgSizeTooLarge.setTokens({name:M,size:T,maxSize:r.maxFileSize}),void y(S,B,U,x);if(null!==r.minFileSize&&Z<=t.getNum(r.minFileSize))return S=r.msgSizeTooSmall.setTokens({name:M,size:T,minSize:r.minFileSize}),void y(S,B,U,x);if(!t.isEmpty(g)&&t.isArray(g)){for(E=0;Eb)return r.addToStack(B),l.addClass("file-thumb-loading"),r._previewDefault(B,U),r._initFileActions(),r._updateFileDetails(u),void a(x+1);s.length&&void 0!==FileReader?(D=R(B.type,M),z=L(B.type,M),$=O(B.type,M),d.html(c.replace("{index}",x+1).replace("{files}",u)),l.addClass("file-thumb-loading"),o.onerror=function(e){r._errorHandler(e,M)},o.onload=function(i){var a,n,s,l,d,c,h,p,u=[];if(n={name:M,type:B.type},e.each(f,function(e,t){"object"!==e&&"other"!==e&&t(B.type,M)&&V++}),0===V){for(s=new Uint8Array(i.target.result),E=0;E0)for(t=0;t0?a.initialCaption:"",a.$caption.attr("title","").val(i),t.addCss(a.$container,"file-input-new"),a._validateDefaultPreview()),0===a.$container.find(t.FRAMES).length&&(a._initCaption()||a.$captionContainer.removeClass("icon-visible")),a._hideFileIcon(),a._raise("filecleared"),a.$captionContainer.focus(),a._setFileDropZoneTitle(),a.$element},reset:function(){if(this._raise("filereset"))return this._resetPreview(),this.$container.find(".fileinput-filename").text(""),t.addCss(this.$container,"file-input-new"),(this.getFrames().length||this.dropZoneEnabled)&&this.$container.removeClass("file-input-new"),this.clearStack(),this.formdata={},this._setFileDropZoneTitle(),this.$element},disable:function(){return this.isDisabled=!0,this._raise("filedisabled"),this.$element.attr("disabled","disabled"),this.$container.find(".kv-fileinput-caption").addClass("file-caption-disabled"),this.$container.find(".fileinput-remove, .fileinput-upload, .file-preview-frame button").attr("disabled",!0),t.addCss(this.$container.find(".btn-file"),"disabled"),this._initDragDrop(),this.$element},enable:function(){return this.isDisabled=!1,this._raise("fileenabled"),this.$element.removeAttr("disabled"),this.$container.find(".kv-fileinput-caption").removeClass("file-caption-disabled"),this.$container.find(".fileinput-remove, .fileinput-upload, .file-preview-frame button").removeAttr("disabled"),this.$container.find(".btn-file").removeClass("disabled"),this._initDragDrop(),this.$element},upload:function(){var i,a,r,n=this.getFileStack().length,s=!e.isEmptyObject(this._getExtraData());if(this.isAjaxUpload&&!this.isDisabled&&this._isFileSelectionValid(n))if(this._resetUpload(),0!==n||s)if(this.$progress.show(),this.uploadCount=0,this.uploadStatus={},this.uploadLog=[],this.lock(),this._setProgress(2),0===n&&s)this._uploadExtraOnly();else{if(r=this.filestack.length,this.hasInitData=!1,!this.uploadAsync)return this._uploadBatch(),this.$element;for(a=this._getOutData(),this._raise("filebatchpreupload",[a]),this.fileBatchCompleted=!1,this.uploadCache={content:[],config:[],tags:[],append:!0},this.uploadAsyncCount=this.getFileStack().length,i=0;i',next:' ',toggleheader:' ',fullscreen:' ',borderless:' ',close:' '},previewZoomButtonClasses:{prev:"btn btn-navigate",next:"btn btn-navigate",toggleheader:"btn btn-sm btn-kv btn-default btn-outline-secondary",fullscreen:"btn btn-sm btn-kv btn-default btn-outline-secondary",borderless:"btn btn-sm btn-kv btn-default btn-outline-secondary",close:"btn btn-sm btn-kv btn-default btn-outline-secondary"},previewTemplates:{},previewContentTemplates:{},preferIconicPreview:!1,preferIconicZoomPreview:!1,allowedPreviewTypes:void 0,allowedPreviewMimeTypes:null,allowedFileTypes:null,allowedFileExtensions:null,defaultPreviewContent:null,customLayoutTags:{},customPreviewTags:{},previewFileIcon:' ',previewFileIconClass:"file-other-icon",previewFileIconSettings:{},previewFileExtSettings:{},buttonLabelClass:"hidden-xs",browseIcon:' ',browseClass:"btn btn-primary",removeIcon:' ',removeClass:"btn btn-default btn-secondary",cancelIcon:' ',cancelClass:"btn btn-default btn-secondary",uploadIcon:' ',uploadClass:"btn btn-default btn-secondary",uploadUrl:null,uploadUrlThumb:null,uploadAsync:!0,uploadExtraData:{},zoomModalHeight:480,minImageWidth:null,minImageHeight:null,maxImageWidth:null,maxImageHeight:null,resizeImage:!1,resizePreference:"width",resizeQuality:.92,resizeDefaultImageType:"image/jpeg",resizeIfSizeMoreThan:0,minFileSize:0,maxFileSize:0,maxFilePreviewSize:25600,minFileCount:0,maxFileCount:0,validateInitialCount:!1,msgValidationErrorClass:"text-danger",msgValidationErrorIcon:' ',msgErrorClass:"file-error-message",progressThumbClass:"progress-bar bg-success progress-bar-success progress-bar-striped active",progressClass:"progress-bar bg-success progress-bar-success progress-bar-striped active",progressCompleteClass:"progress-bar bg-success progress-bar-success",progressErrorClass:"progress-bar bg-danger progress-bar-danger",progressUploadThreshold:99,previewFileType:"image",elCaptionContainer:null,elCaptionText:null,elPreviewContainer:null,elPreviewImage:null,elPreviewStatus:null,elErrorContainer:null,errorCloseButton:t.closeButton("kv-error-close"),slugCallback:null,dropZoneEnabled:!0,dropZoneTitleClass:"file-drop-zone-title",fileActionSettings:{},otherActionButtons:"",textEncoding:"UTF-8",ajaxSettings:{},ajaxDeleteSettings:{},showAjaxErrorDetails:!0,mergeAjaxCallbacks:!1,mergeAjaxDeleteCallbacks:!1,retryErrorUploads:!0,reversePreviewOrder:!1},e.fn.fileinputLocales.en={fileSingle:"file",filePlural:"files",browseLabel:"Browse …",removeLabel:"Remove",removeTitle:"Clear selected files",cancelLabel:"Cancel",cancelTitle:"Abort ongoing upload",uploadLabel:"Upload",uploadTitle:"Upload selected files",msgNo:"No",msgNoFilesSelected:"No files selected",msgCancelled:"Cancelled",msgPlaceholder:"Select {files}...",msgZoomModalHeading:"Detailed Preview",msgFileRequired:"You must select a file to upload.",msgSizeTooSmall:'File "{name}" ({size} KB ) is too small and must be larger than {minSize} KB .',msgSizeTooLarge:'File "{name}" ({size} KB ) exceeds maximum allowed upload size of {maxSize} KB .',msgFilesTooLess:"You must select at least {n} {files} to upload.",msgFilesTooMany:"Number of files selected for upload ({n}) exceeds maximum allowed limit of {m} .",msgFileNotFound:'File "{name}" not found!',msgFileSecured:'Security restrictions prevent reading the file "{name}".',msgFileNotReadable:'File "{name}" is not readable.',msgFilePreviewAborted:'File preview aborted for "{name}".',msgFilePreviewError:'An error occurred while reading the file "{name}".',msgInvalidFileName:'Invalid or unsupported characters in file name "{name}".',msgInvalidFileType:'Invalid type for file "{name}". Only "{types}" files are supported.',msgInvalidFileExtension:'Invalid extension for file "{name}". Only "{extensions}" files are supported.',msgFileTypes:{image:"image",html:"HTML",text:"text",video:"video",audio:"audio",flash:"flash",pdf:"PDF",object:"object"},msgUploadAborted:"The file upload was aborted",msgUploadThreshold:"Processing...",msgUploadBegin:"Initializing...",msgUploadEnd:"Done",msgUploadEmpty:"No valid data available for upload.",msgUploadError:"Error",msgValidationError:"Validation Error",msgLoading:"Loading file {index} of {files} …",msgProgress:"Loading file {index} of {files} - {name} - {percent}% completed.",msgSelected:"{n} {files} selected",msgFoldersNotAllowed:"Drag & drop files only! {n} folder(s) dropped were skipped.",msgImageWidthSmall:'Width of image file "{name}" must be at least {size} px.',msgImageHeightSmall:'Height of image file "{name}" must be at least {size} px.',msgImageWidthLarge:'Width of image file "{name}" cannot exceed {size} px.',msgImageHeightLarge:'Height of image file "{name}" cannot exceed {size} px.',msgImageResizeError:"Could not get the image dimensions to resize.",msgImageResizeException:"Error while resizing the image.{errors} ",msgAjaxError:"Something went wrong with the {operation} operation. Please try again later!",msgAjaxProgressError:"{operation} failed",ajaxOperations:{deleteThumb:"file delete",uploadThumb:"file upload",uploadBatch:"batch file upload",uploadExtra:"form data upload"},dropZoneTitle:"Drag & drop files here …",dropZoneClickTitle:" (or click to select {files})",previewZoomButtonTitles:{prev:"View previous file",next:"View next file",toggleheader:"Toggle header",fullscreen:"Toggle full screen",borderless:"Toggle borderless mode",close:"Close detailed preview"},usePdfRenderer:function(){return!!navigator.userAgent.match(/(iPod|iPhone|iPad|Android)/i)},pdfRendererUrl:"",pdfRendererTemplate:''},e.fn.fileinput.Constructor=i,e(document).ready(function(){var t=e("input.file[type=file]");t.length&&t.fileinput()})});
\ No newline at end of file
diff --git a/public/static/bootstrap-fileinput/4.5.1/js/locales/zh.js b/public/static/bootstrap-fileinput/4.5.1/js/locales/zh.js
new file mode 100644
index 00000000..32f40574
--- /dev/null
+++ b/public/static/bootstrap-fileinput/4.5.1/js/locales/zh.js
@@ -0,0 +1,100 @@
+/*!
+ * FileInput Chinese Translations
+ *
+ * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or
+ * any HTML markup tags in the messages must not be converted or translated.
+ *
+ * @see http://github.com/kartik-v/bootstrap-fileinput
+ * @author kangqf
+ *
+ * NOTE: this file must be saved in UTF-8 encoding.
+ */
+(function ($) {
+ "use strict";
+
+ $.fn.fileinputLocales['zh'] = {
+ fileSingle: '文件',
+ filePlural: '个文件',
+ browseLabel: '选择 …',
+ removeLabel: '移除',
+ removeTitle: '清除选中文件',
+ cancelLabel: '取消',
+ cancelTitle: '取消进行中的上传',
+ uploadLabel: '上传',
+ uploadTitle: '上传选中文件',
+ msgNo: '没有',
+ msgNoFilesSelected: '未选择文件',
+ msgCancelled: '取消',
+ msgPlaceholder: '选择 {files}...',
+ msgZoomModalHeading: '详细预览',
+ msgFileRequired: '必须选择一个文件上传.',
+ msgSizeTooSmall: '文件 "{name}" ({size} KB ) 必须大于限定大小 {minSize} KB .',
+ msgSizeTooLarge: '文件 "{name}" ({size} KB ) 超过了允许大小 {maxSize} KB .',
+ msgFilesTooLess: '你必须选择最少 {n} {files} 来上传. ',
+ msgFilesTooMany: '选择的上传文件个数 ({n}) 超出最大文件的限制个数 {m} .',
+ msgFileNotFound: '文件 "{name}" 未找到!',
+ msgFileSecured: '安全限制,为了防止读取文件 "{name}".',
+ msgFileNotReadable: '文件 "{name}" 不可读.',
+ msgFilePreviewAborted: '取消 "{name}" 的预览.',
+ msgFilePreviewError: '读取 "{name}" 时出现了一个错误.',
+ msgInvalidFileName: '文件名 "{name}" 包含非法字符.',
+ msgInvalidFileType: '不正确的类型 "{name}". 只支持 "{types}" 类型的文件.',
+ msgInvalidFileExtension: '不正确的文件扩展名 "{name}". 只支持 "{extensions}" 的文件扩展名.',
+ msgFileTypes: {
+ 'image': 'image',
+ 'html': 'HTML',
+ 'text': 'text',
+ 'video': 'video',
+ 'audio': 'audio',
+ 'flash': 'flash',
+ 'pdf': 'PDF',
+ 'object': 'object'
+ },
+ msgUploadAborted: '该文件上传被中止',
+ msgUploadThreshold: '处理中...',
+ msgUploadBegin: '正在初始化...',
+ msgUploadEnd: '完成',
+ msgUploadEmpty: '无效的文件上传.',
+ msgUploadError: '上传出错',
+ msgValidationError: '验证错误',
+ msgLoading: '加载第 {index} 文件 共 {files} …',
+ msgProgress: '加载第 {index} 文件 共 {files} - {name} - {percent}% 完成.',
+ msgSelected: '{n} {files} 选中',
+ msgFoldersNotAllowed: '只支持拖拽文件! 跳过 {n} 拖拽的文件夹.',
+ msgImageWidthSmall: '图像文件的"{name}"的宽度必须是至少{size}像素.',
+ msgImageHeightSmall: '图像文件的"{name}"的高度必须至少为{size}像素.',
+ msgImageWidthLarge: '图像文件"{name}"的宽度不能超过{size}像素.',
+ msgImageHeightLarge: '图像文件"{name}"的高度不能超过{size}像素.',
+ msgImageResizeError: '无法获取的图像尺寸调整。',
+ msgImageResizeException: '调整图像大小时发生错误。{errors} ',
+ msgAjaxError: '{operation} 发生错误. 请重试!',
+ msgAjaxProgressError: '{operation} 失败',
+ ajaxOperations: {
+ deleteThumb: '删除文件',
+ uploadThumb: '上传文件',
+ uploadBatch: '批量上传',
+ uploadExtra: '表单数据上传'
+ },
+ dropZoneTitle: '拖拽文件到这里 … 支持多文件同时上传',
+ dropZoneClickTitle: ' (或点击{files}按钮选择文件)',
+ fileActionSettings: {
+ removeTitle: '删除文件',
+ uploadTitle: '上传文件',
+ uploadRetryTitle: '重试',
+ zoomTitle: '查看详情',
+ dragTitle: '移动 / 重置',
+ indicatorNewTitle: '没有上传',
+ indicatorSuccessTitle: '上传',
+ indicatorErrorTitle: '上传错误',
+ indicatorLoadingTitle: '上传 ...'
+ },
+ previewZoomButtonTitles: {
+ prev: '预览上一个文件',
+ next: '预览下一个文件',
+ toggleheader: '缩放',
+ fullscreen: '全屏',
+ borderless: '无边界模式',
+ close: '关闭当前预览'
+ }
+ };
+})(window.jQuery);
diff --git a/public/static/bootstrap/3.3.7/css/bootstrap.min.css b/public/static/bootstrap/3.3.7/css/bootstrap.min.css
new file mode 100644
index 00000000..ed3905e0
--- /dev/null
+++ b/public/static/bootstrap/3.3.7/css/bootstrap.min.css
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-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{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}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{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;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-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 00000000..b93a4953
Binary files /dev/null and b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot differ
diff --git a/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg
new file mode 100644
index 00000000..94fb5490
--- /dev/null
+++ b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 00000000..1413fc60
Binary files /dev/null and b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf differ
diff --git a/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 00000000..9e612858
Binary files /dev/null and b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff differ
diff --git a/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2 b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2
new file mode 100644
index 00000000..64539b54
Binary files /dev/null and b/public/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2 differ
diff --git a/public/static/bootstrap/3.3.7/js/bootstrap.min.js b/public/static/bootstrap/3.3.7/js/bootstrap.min.js
new file mode 100644
index 00000000..9bcd2fcc
--- /dev/null
+++ b/public/static/bootstrap/3.3.7/js/bootstrap.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under the MIT license
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-m
o.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
\ No newline at end of file
diff --git a/public/static/clipboard.js/2.0.1/clipboard.min.js b/public/static/clipboard.js/2.0.1/clipboard.min.js
new file mode 100644
index 00000000..5f8ec260
--- /dev/null
+++ b/public/static/clipboard.js/2.0.1/clipboard.min.js
@@ -0,0 +1,7 @@
+/*!
+ * clipboard.js v2.0.1
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT 漏 Zeno Rocha
+ */
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])});
\ No newline at end of file
diff --git a/public/static/contextjs/css/context.standalone.css b/public/static/contextjs/css/context.standalone.css
new file mode 100644
index 00000000..22dd001c
--- /dev/null
+++ b/public/static/contextjs/css/context.standalone.css
@@ -0,0 +1,228 @@
+/**
+ * ContextJS Styles
+ * For use WITHOUT Twitters Bootstrap CSS
+ */
+
+.nav-header {
+ display: block;
+ padding: 3px 15px;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 20px;
+ color: #999;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-transform: uppercase;
+}
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ background-color: #ffffff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ *border-right-width: 2px;
+ *border-bottom-width: 2px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+ text-align:left;
+}
+.dropdown-menu.pull-right {
+ right: 0;
+ left: auto;
+}
+.dropdown-menu .divider {
+ *width: 100%;
+ height: 1px;
+ margin: 9px 1px;
+ *margin: -5px 0 5px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+}
+.dropdown-menu a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ line-height: 20px;
+ color: #333333;
+ white-space: nowrap;
+ text-decoration: none;
+}
+.dropdown-menu li > a:hover, .dropdown-menu li > a:focus, .dropdown-submenu:hover > a {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #0088cc;
+ background-color: #0081c2;
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3);
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3);
+ background-repeat: repeat-x;
+ filter: progid: dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+.dropdown-menu .active > a, .dropdown-menu .active > a:hover {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #0088cc;
+ background-color: #0081c2;
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3);
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3);
+ background-repeat: repeat-x;
+ outline: 0;
+ filter: progid
+ : dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
+}
+.dropdown-menu .disabled > a, .dropdown-menu .disabled > a:hover {
+ color: #999999;
+}
+.dropdown-menu .disabled > a:hover {
+ text-decoration: none;
+ cursor: default;
+ background-color: transparent;
+}
+.open {
+ *z-index: 1000;
+}
+.open > .dropdown-menu {
+ display: block;
+}
+.pull-right > .dropdown-menu {
+ right: 0;
+ left: auto;
+}
+.dropup .caret, .navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid #000000;
+ content: "\2191";
+}
+.dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px;
+}
+.dropdown-submenu {
+ position: relative;
+}
+.dropdown-submenu > .dropdown-menu {
+ top: 0;
+ left: 100%;
+ margin-top: -6px;
+ margin-left: -1px;
+ -webkit-border-radius: 0 6px 6px 6px;
+ -moz-border-radius: 0 6px 6px 6px;
+ border-radius: 0 6px 6px 6px;
+}
+.dropdown-submenu > .dropdown-menu.drop-left{
+ left:-100%;
+}
+.dropdown-submenu:hover .dropdown-menu {
+ display: block;
+}
+.dropdown-submenu > a:after {
+ display: block;
+ float: right;
+ width: 0;
+ height: 0;
+ margin-top: 5px;
+ margin-right: -10px;
+ border-color: transparent;
+ border-left-color: #cccccc;
+ border-style: solid;
+ border-width: 5px 0 5px 5px;
+ content: " ";
+}
+.dropdown-submenu:hover > a:after {
+ border-left-color: #ffffff;
+}
+.dropdown .dropdown-menu .nav-header {
+ padding-right: 20px;
+ padding-left: 20px;
+}
+/**
+ * Context Styles
+ */
+
+.dropdown-context .nav-header {
+ cursor: default;
+}
+.dropdown-context:before, .dropdown-context-up:before {
+ position: absolute;
+ top: -7px;
+ left: 9px;
+ display: inline-block;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-left: 7px solid transparent;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ content: '';
+}
+.dropdown-context:after, .dropdown-context-up:after {
+ position: absolute;
+ top: -6px;
+ left: 10px;
+ display: inline-block;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #ffffff;
+ border-left: 6px solid transparent;
+ content: '';
+}
+.dropdown-context-up:before, .dropdown-context-up:after {
+ top: auto;
+ bottom: -7px;
+ z-index: 9999;
+}
+.dropdown-context-up:before {
+ border-right: 7px solid transparent;
+ border-top: 7px solid #ccc;
+ border-bottom: none;
+ border-left: 7px solid transparent;
+}
+.dropdown-context-up:after {
+ border-right: 6px solid transparent;
+ border-top: 6px solid #ffffff;
+ border-left: 6px solid transparent;
+ border-bottom: none;
+}
+.dropdown-context-sub:before, .dropdown-context-sub:after {
+ display: none;
+}
+.dropdown-context .dropdown-submenu:hover .dropdown-menu {
+ display: none;
+}
+.dropdown-context .dropdown-submenu:hover > .dropdown-menu {
+ display: block;
+}
+
+.compressed-context a{
+ padding-left: 14px;
+ padding-top: 0;
+ padding-bottom: 0;
+ font-size: 13px;
+}
+.compressed-context .divider{
+ margin: 5px 1px;
+}
+.compressed-context .nav-header{
+ padding:1px 13px;
+}
\ No newline at end of file
diff --git a/public/static/contextjs/js/context.js b/public/static/contextjs/js/context.js
new file mode 100644
index 00000000..b0de0efb
--- /dev/null
+++ b/public/static/contextjs/js/context.js
@@ -0,0 +1,141 @@
+/*
+ * Context.js
+ * Copyright Jacob Kelley
+ * MIT License
+ */
+
+var context = context || (function () {
+
+ var options = {
+ fadeSpeed: 100,
+ filter: function ($obj) {
+ // Modify $obj, Do not return
+ },
+ above: 'auto',
+ preventDoubleContext: true,
+ compress: false
+ };
+
+ function initialize(opts) {
+
+ options = $.extend({}, options, opts);
+
+ $(document).on('click', 'html', function () {
+ $('.dropdown-context').fadeOut(options.fadeSpeed, function(){
+ $('.dropdown-context').css({display:''}).find('.drop-left').removeClass('drop-left');
+ });
+ });
+ if(options.preventDoubleContext){
+ $(document).on('contextmenu', '.dropdown-context', function (e) {
+ e.preventDefault();
+ });
+ }
+ $(document).on('mouseenter', '.dropdown-submenu', function(){
+ var $sub = $(this).find('.dropdown-context-sub:first'),
+ subWidth = $sub.width(),
+ subLeft = $sub.offset().left,
+ collision = (subWidth+subLeft) > window.innerWidth;
+ if(collision){
+ $sub.addClass('drop-left');
+ }
+ });
+
+ }
+
+ function updateOptions(opts){
+ options = $.extend({}, options, opts);
+ }
+
+ function buildMenu(data, id, subMenu) {
+ var subClass = (subMenu) ? ' dropdown-context-sub' : '',
+ compressed = options.compress ? ' compressed-context' : '',
+ $menu = $('');
+ var i = 0, linkTarget = '';
+ for(i; i');
+ } else if (typeof data[i].header !== 'undefined') {
+ $menu.append('');
+ } else {
+ if (typeof data[i].href == 'undefined') {
+ data[i].href = '#';
+ }
+ if (typeof data[i].target !== 'undefined') {
+ linkTarget = ' target="'+data[i].target+'"';
+ }
+ if (typeof data[i].subMenu !== 'undefined') {
+ $sub = ('');
+ } else {
+ $sub = $('' + data[i].text + ' ');
+ }
+ if (typeof data[i].action !== 'undefined') {
+ var actiond = new Date(),
+ actionID = 'event-' + actiond.getTime() * Math.floor(Math.random()*100000),
+ eventAction = data[i].action;
+ $sub.find('a').attr('id', actionID);
+ $('#' + actionID).addClass('context-event');
+ $(document).on('click', '#' + actionID, eventAction);
+ }
+ $menu.append($sub);
+ if (typeof data[i].subMenu != 'undefined') {
+ var subMenuData = buildMenu(data[i].subMenu, id, true);
+ $menu.find('li:last').append(subMenuData);
+ }
+ }
+ if (typeof options.filter == 'function') {
+ options.filter($menu.find('li:last'));
+ }
+ }
+ return $menu;
+ }
+
+ function addContext(selector, data) {
+
+ var d = new Date(),
+ id = d.getTime(),
+ $menu = buildMenu(data, id);
+
+ $('body').append($menu);
+
+
+ $(document).on('contextmenu', selector, function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ $('.dropdown-context:not(.dropdown-context-sub)').hide();
+
+ $dd = $('#dropdown-' + id);
+ if (typeof options.above == 'boolean' && options.above) {
+ $dd.addClass('dropdown-context-up').css({
+ top: e.pageY - 20 - $('#dropdown-' + id).height(),
+ left: e.pageX - 13
+ }).fadeIn(options.fadeSpeed);
+ } else if (typeof options.above == 'string' && options.above == 'auto') {
+ $dd.removeClass('dropdown-context-up');
+ var autoH = $dd.height() + 12;
+ if ((e.pageY + autoH) > $('html').height()) {
+ $dd.addClass('dropdown-context-up').css({
+ top: e.pageY - 20 - autoH,
+ left: e.pageX - 13
+ }).fadeIn(options.fadeSpeed);
+ } else {
+ $dd.css({
+ top: e.pageY + 10,
+ left: e.pageX - 13
+ }).fadeIn(options.fadeSpeed);
+ }
+ }
+ });
+ }
+
+ function destroyContext(selector) {
+ $(document).off('contextmenu', selector).off('click', '.context-event');
+ }
+
+ return {
+ init: initialize,
+ settings: updateOptions,
+ attach: addContext,
+ destroy: destroyContext
+ };
+})();
\ No newline at end of file
diff --git a/public/static/jquery-viewer/1.2.0/css/viewer.css b/public/static/jquery-viewer/1.2.0/css/viewer.css
new file mode 100644
index 00000000..43ff5d10
--- /dev/null
+++ b/public/static/jquery-viewer/1.2.0/css/viewer.css
@@ -0,0 +1,455 @@
+/*!
+ * Viewer.js v1.2.0
+ * https://fengyuanchen.github.io/viewerjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2018-07-15T10:09:17.532Z
+ */
+
+.viewer-zoom-in::before,
+.viewer-zoom-out::before,
+.viewer-one-to-one::before,
+.viewer-reset::before,
+.viewer-prev::before,
+.viewer-play::before,
+.viewer-next::before,
+.viewer-rotate-left::before,
+.viewer-rotate-right::before,
+.viewer-flip-horizontal::before,
+.viewer-flip-vertical::before,
+.viewer-fullscreen::before,
+.viewer-fullscreen-exit::before,
+.viewer-close::before {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAAUCAYAAABWOyJDAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAQPSURBVHic7Zs/iFxVFMa/0U2UaJGksUgnIVhYxVhpjDbZCBmLdAYECxsRFBTUamcXUiSNncgKQbSxsxH8gzAP3FU2jY0kKKJNiiiIghFlccnP4p3nPCdv3p9778vsLOcHB2bfveeb7955c3jvvNkBIMdxnD64a94GHMfZu3iBcRynN7zAOI7TG15gHCeeNUkr8zaxG2lbYDYsdgMbktBsP03jdQwljSXdtBhLOmtjowC9Mg9L+knSlcD8TNKpSA9lBpK2JF2VdDSR5n5J64m0qli399hNFMUlpshQii5jbXTbHGviB0nLNeNDSd9VO4A2UdB2fp+x0eCnaXxWXGA2X0au/3HgN9P4LFCjIANOJdrLr0zzZ+BEpNYDwKbpnQMeAw4m8HjQtM6Z9qa917zPQwFr3M5KgA6J5rTJCdFZJj9/lyvGhsDvwFNVuV2MhhjrK6b9bFiE+j1r87eBl4HDwCF7/U/k+ofAX5b/EXBv5JoLMuILzf3Ap6Z3EzgdqHMCuF7hcQf4HDgeoHnccncqdK/TvSDWffFXI/exICY/xZyqc6XLWF1UFZna4gJ7q8BsRvgd2/xXpo6P+D9dfT7PpECtA3cnWPM0GXGFZh/wgWltA+cDNC7X+AP4GzjZQe+k5dRxuYPeiuXU7e1qwLpDz7dFjXKRaSwuMLvAlG8zZlG+YmiK1HoFqT7wP2z+4Q45TfEGcMt01xLoNZEBTwRqD4BLpnMLeC1A41UmVxsXgXeBayV/Wx20rpTyrpnWRft7p6O/FdqzGrDukPNtkaMoMo3FBdBSQMOnYBCReyf05s126fU9ytfX98+mY54Kxnp7S9K3kj6U9KYdG0h6UdLbkh7poFXMfUnSOyVvL0h6VtIXHbS6nOP+s/Zm9mvyXW1uuC9ohZ72E9uDmXWLJOB1GxsH+DxPftsB8B6wlGDN02TAkxG6+4D3TWsbeC5CS8CDFce+AW500LhhOW2020TRjK3b21HEmgti9m0RonxbdMZeVzV+/4tF3cBpP7E9mKHNL5q8h5g0eYsCMQz0epq8gQrwMXAgcs0FGXGFRcB9wCemF9PkbYqM/Bas7fxLwNeJPdTdpo4itQti8lPMqTpXuozVRVXPpbHI3KkNTB1NfkL81j2mvhDp91HgV9MKuRIqrykj3WPq4rHyL+axj8/qGPmTqi6F9YDlHOvJU6oYcTsh/TYSzWmTE6JT19CtLTJt32D6CmHe0eQn1O8z5AXgT4sx4Vcu0/EQecMydB8z0hUWkTd2t4CrwNEePqMBcAR4mrBbwyXLPWJa8zrXmmLEhNBmfpkuY2102xxrih+pb+ieAb6vGhuA97UcJ5KR8gZ77K+99xxeYBzH6Q3/Z0fHcXrDC4zjOL3hBcZxnN74F+zlvXFWXF9PAAAAAElFTkSuQmCC');
+ background-repeat: no-repeat;
+ color: transparent;
+ display: block;
+ font-size: 0;
+ height: 20px;
+ line-height: 0;
+ width: 20px;
+}
+
+.viewer-zoom-in::before {
+ background-position: 0 0;
+ content: 'Zoom In';
+}
+
+.viewer-zoom-out::before {
+ background-position: -20px 0;
+ content: 'Zoom Out';
+}
+
+.viewer-one-to-one::before {
+ background-position: -40px 0;
+ content: 'One to One';
+}
+
+.viewer-reset::before {
+ background-position: -60px 0;
+ content: 'Reset';
+}
+
+.viewer-prev::before {
+ background-position: -80px 0;
+ content: 'Previous';
+}
+
+.viewer-play::before {
+ background-position: -100px 0;
+ content: 'Play';
+}
+
+.viewer-next::before {
+ background-position: -120px 0;
+ content: 'Next';
+}
+
+.viewer-rotate-left::before {
+ background-position: -140px 0;
+ content: 'Rotate Left';
+}
+
+.viewer-rotate-right::before {
+ background-position: -160px 0;
+ content: 'Rotate Right';
+}
+
+.viewer-flip-horizontal::before {
+ background-position: -180px 0;
+ content: 'Flip Horizontal';
+}
+
+.viewer-flip-vertical::before {
+ background-position: -200px 0;
+ content: 'Flip Vertical';
+}
+
+.viewer-fullscreen::before {
+ background-position: -220px 0;
+ content: 'Enter Full Screen';
+}
+
+.viewer-fullscreen-exit::before {
+ background-position: -240px 0;
+ content: 'Exit Full Screen';
+}
+
+.viewer-close::before {
+ background-position: -260px 0;
+ content: 'Close';
+}
+
+.viewer-container {
+ bottom: 0;
+ direction: ltr;
+ font-size: 0;
+ left: 0;
+ line-height: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ -webkit-tap-highlight-color: transparent;
+ top: 0;
+ -webkit-touch-callout: none;
+ -ms-touch-action: none;
+ touch-action: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.viewer-container::-moz-selection,
+.viewer-container *::-moz-selection {
+ background-color: transparent;
+}
+
+.viewer-container::selection,
+.viewer-container *::selection {
+ background-color: transparent;
+}
+
+.viewer-container img {
+ display: block;
+ height: auto;
+ max-height: none !important;
+ max-width: none !important;
+ min-height: 0 !important;
+ min-width: 0 !important;
+ width: 100%;
+}
+
+.viewer-canvas {
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.viewer-canvas > img {
+ height: auto;
+ margin: 15px auto;
+ max-width: 90% !important;
+ width: auto;
+}
+
+.viewer-footer {
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ text-align: center;
+}
+
+.viewer-navbar {
+ background-color: rgba(0, 0, 0, .5);
+ overflow: hidden;
+}
+
+.viewer-list {
+ -webkit-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 50px;
+ margin: 0;
+ overflow: hidden;
+ padding: 1px 0;
+}
+
+.viewer-list > li {
+ color: transparent;
+ cursor: pointer;
+ float: left;
+ font-size: 0;
+ height: 50px;
+ line-height: 0;
+ opacity: .5;
+ overflow: hidden;
+ -webkit-transition: opacity .15s;
+ transition: opacity .15s;
+ width: 30px;
+}
+
+.viewer-list > li:hover {
+ opacity: .75;
+}
+
+.viewer-list > li + li {
+ margin-left: 1px;
+}
+
+.viewer-list > .viewer-loading {
+ position: relative;
+}
+
+.viewer-list > .viewer-loading::after {
+ border-width: 2px;
+ height: 20px;
+ margin-left: -10px;
+ margin-top: -10px;
+ width: 20px;
+}
+
+.viewer-list > .viewer-active,
+.viewer-list > .viewer-active:hover {
+ opacity: 1;
+}
+
+.viewer-player {
+ background-color: #000;
+ bottom: 0;
+ cursor: none;
+ display: none;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.viewer-player > img {
+ left: 0;
+ position: absolute;
+ top: 0;
+}
+
+.viewer-toolbar > ul {
+ display: inline-block;
+ margin: 0 auto 5px;
+ overflow: hidden;
+ padding: 3px 0;
+}
+
+.viewer-toolbar > ul > li {
+ background-color: rgba(0, 0, 0, .5);
+ border-radius: 50%;
+ cursor: pointer;
+ float: left;
+ height: 24px;
+ overflow: hidden;
+ -webkit-transition: background-color .15s;
+ transition: background-color .15s;
+ width: 24px;
+}
+
+.viewer-toolbar > ul > li:hover {
+ background-color: rgba(0, 0, 0, .8);
+}
+
+.viewer-toolbar > ul > li::before {
+ margin: 2px;
+}
+
+.viewer-toolbar > ul > li + li {
+ margin-left: 1px;
+}
+
+.viewer-toolbar > ul > .viewer-small {
+ height: 18px;
+ margin-bottom: 3px;
+ margin-top: 3px;
+ width: 18px;
+}
+
+.viewer-toolbar > ul > .viewer-small::before {
+ margin: -1px;
+}
+
+.viewer-toolbar > ul > .viewer-large {
+ height: 30px;
+ margin-bottom: -3px;
+ margin-top: -3px;
+ width: 30px;
+}
+
+.viewer-toolbar > ul > .viewer-large::before {
+ margin: 5px;
+}
+
+.viewer-tooltip {
+ background-color: rgba(0, 0, 0, 0.8);
+ border-radius: 10px;
+ color: #fff;
+ display: none;
+ font-size: 12px;
+ height: 20px;
+ left: 50%;
+ line-height: 20px;
+ margin-left: -25px;
+ margin-top: -10px;
+ position: absolute;
+ text-align: center;
+ top: 50%;
+ width: 50px;
+}
+
+.viewer-title {
+ color: #ccc;
+ display: inline-block;
+ font-size: 12px;
+ line-height: 1;
+ margin: 0 5% 5px;
+ max-width: 90%;
+ opacity: .8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-transition: opacity .15s;
+ transition: opacity .15s;
+ white-space: nowrap;
+}
+
+.viewer-title:hover {
+ opacity: 1;
+}
+
+.viewer-button {
+ background-color: rgba(0, 0, 0, .5);
+ border-radius: 50%;
+ cursor: pointer;
+ height: 80px;
+ overflow: hidden;
+ position: absolute;
+ right: -40px;
+ top: -40px;
+ -webkit-transition: background-color .15s;
+ transition: background-color .15s;
+ width: 80px;
+}
+
+.viewer-button:focus,
+.viewer-button:hover {
+ background-color: rgba(0, 0, 0, .8);
+}
+
+.viewer-button::before {
+ bottom: 15px;
+ left: 15px;
+ position: absolute;
+}
+
+.viewer-fixed {
+ position: fixed;
+}
+
+.viewer-open {
+ overflow: hidden;
+}
+
+.viewer-show {
+ display: block;
+}
+
+.viewer-hide {
+ display: none;
+}
+
+.viewer-backdrop {
+ background-color: rgba(0, 0, 0, .5);
+}
+
+.viewer-invisible {
+ visibility: hidden;
+}
+
+.viewer-move {
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+
+.viewer-fade {
+ opacity: 0;
+}
+
+.viewer-in {
+ opacity: 1;
+}
+
+.viewer-transition {
+ -webkit-transition: all .3s;
+ transition: all .3s;
+}
+
+@-webkit-keyframes viewer-spinner {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes viewer-spinner {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+.viewer-loading::after {
+ -webkit-animation: viewer-spinner 1s linear infinite;
+ animation: viewer-spinner 1s linear infinite;
+ border: 4px solid rgba(255, 255, 255, .1);
+ border-left-color: rgba(255, 255, 255, .5);
+ border-radius: 50%;
+ content: '';
+ display: inline-block;
+ height: 40px;
+ left: 50%;
+ margin-left: -20px;
+ margin-top: -20px;
+ position: absolute;
+ top: 50%;
+ width: 40px;
+ z-index: 1;
+}
+
+@media (max-width: 767px) {
+ .viewer-hide-xs-down {
+ display: none;
+ }
+}
+
+@media (max-width: 991px) {
+ .viewer-hide-sm-down {
+ display: none;
+ }
+}
+
+@media (max-width: 1199px) {
+ .viewer-hide-md-down {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/public/static/jquery-viewer/1.2.0/js/jquery-viewer.js b/public/static/jquery-viewer/1.2.0/js/jquery-viewer.js
new file mode 100644
index 00000000..dcf0cbc9
--- /dev/null
+++ b/public/static/jquery-viewer/1.2.0/js/jquery-viewer.js
@@ -0,0 +1,75 @@
+/*!
+ * jQuery Viewer v1.0.0
+ * https://github.com/fengyuanchen/jquery-viewer
+ *
+ * Copyright (c) 2018 Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2018-04-01T05:58:29.617Z
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery'), require('viewerjs')) :
+ typeof define === 'function' && define.amd ? define(['jquery', 'viewerjs'], factory) :
+ (factory(global.jQuery,global.Viewer));
+}(this, (function ($,Viewer) { 'use strict';
+
+ $ = $ && $.hasOwnProperty('default') ? $['default'] : $;
+ Viewer = Viewer && Viewer.hasOwnProperty('default') ? Viewer['default'] : Viewer;
+
+ if ($.fn) {
+ var AnotherViewer = $.fn.viewer;
+ var NAMESPACE = 'viewer';
+
+ $.fn.viewer = function jQueryViewer(option) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ var result = void 0;
+
+ this.each(function (i, element) {
+ var $element = $(element);
+ var isDestroy = option === 'destroy';
+ var viewer = $element.data(NAMESPACE);
+
+ if (!viewer) {
+ if (isDestroy) {
+ return;
+ }
+
+ var options = $.extend({}, $element.data(), $.isPlainObject(option) && option);
+
+ viewer = new Viewer(element, options);
+ $element.data(NAMESPACE, viewer);
+ }
+
+ if (typeof option === 'string') {
+ var fn = viewer[option];
+
+ if ($.isFunction(fn)) {
+ result = fn.apply(viewer, args);
+
+ if (result === viewer) {
+ result = undefined;
+ }
+
+ if (isDestroy) {
+ $element.removeData(NAMESPACE);
+ }
+ }
+ }
+ });
+
+ return result !== undefined ? result : this;
+ };
+
+ $.fn.viewer.Constructor = Viewer;
+ $.fn.viewer.setDefaults = Viewer.setDefaults;
+ $.fn.viewer.noConflict = function noConflict() {
+ $.fn.viewer = AnotherViewer;
+ return this;
+ };
+ }
+
+})));
\ No newline at end of file
diff --git a/public/static/jquery-viewer/1.2.0/js/viewer.js b/public/static/jquery-viewer/1.2.0/js/viewer.js
new file mode 100644
index 00000000..c4b2d81a
--- /dev/null
+++ b/public/static/jquery-viewer/1.2.0/js/viewer.js
@@ -0,0 +1,3142 @@
+/*!
+ * Viewer.js v1.2.0
+ * https://fengyuanchen.github.io/viewerjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2018-07-15T10:10:54.376Z
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.Viewer = factory());
+}(this, (function () { 'use strict';
+
+ var DEFAULTS = {
+ /**
+ * Define the initial index of image for viewing.
+ * @type {number}
+ */
+ initialViewIndex: 0,
+
+ /**
+ * Enable inline mode.
+ * @type {boolean}
+ */
+ inline: false,
+
+ /**
+ * Show the button on the top-right of the viewer.
+ * @type {boolean}
+ */
+ button: true,
+
+ /**
+ * Show the navbar.
+ * @type {boolean | number}
+ */
+ navbar: true,
+
+ /**
+ * Specify the visibility and the content of the title.
+ * @type {boolean | number | Function | Array}
+ */
+ title: true,
+
+ /**
+ * Show the toolbar.
+ * @type {boolean | number | Object}
+ */
+ toolbar: true,
+
+ /**
+ * Show the tooltip with image ratio (percentage) when zoom in or zoom out.
+ * @type {boolean}
+ */
+ tooltip: true,
+
+ /**
+ * Enable to move the image.
+ * @type {boolean}
+ */
+ movable: true,
+
+ /**
+ * Enable to zoom the image.
+ * @type {boolean}
+ */
+ zoomable: true,
+
+ /**
+ * Enable to rotate the image.
+ * @type {boolean}
+ */
+ rotatable: true,
+
+ /**
+ * Enable to scale the image.
+ * @type {boolean}
+ */
+ scalable: true,
+
+ /**
+ * Enable CSS3 Transition for some special elements.
+ * @type {boolean}
+ */
+ transition: true,
+
+ /**
+ * Enable to request fullscreen when play.
+ * @type {boolean}
+ */
+ fullscreen: true,
+
+ /**
+ * The amount of time to delay between automatically cycling an image when playing.
+ * @type {number}
+ */
+ interval: 5000,
+
+ /**
+ * Enable keyboard support.
+ * @type {boolean}
+ */
+ keyboard: true,
+
+ /**
+ * Enable a modal backdrop, specify `static` for a backdrop
+ * which doesn't close the modal on click.
+ * @type {boolean}
+ */
+ backdrop: true,
+
+ /**
+ * Indicate if show a loading spinner when load image or not.
+ * @type {boolean}
+ */
+ loading: true,
+
+ /**
+ * Indicate if enable loop viewing or not.
+ * @type {boolean}
+ */
+ loop: true,
+
+ /**
+ * Min width of the viewer in inline mode.
+ * @type {number}
+ */
+ minWidth: 200,
+
+ /**
+ * Min height of the viewer in inline mode.
+ * @type {number}
+ */
+ minHeight: 100,
+
+ /**
+ * Define the ratio when zoom the image by wheeling mouse.
+ * @type {number}
+ */
+ zoomRatio: 0.1,
+
+ /**
+ * Define the min ratio of the image when zoom out.
+ * @type {number}
+ */
+ minZoomRatio: 0.01,
+
+ /**
+ * Define the max ratio of the image when zoom in.
+ * @type {number}
+ */
+ maxZoomRatio: 100,
+
+ /**
+ * Define the CSS `z-index` value of viewer in modal mode.
+ * @type {number}
+ */
+ zIndex: 2015,
+
+ /**
+ * Define the CSS `z-index` value of viewer in inline mode.
+ * @type {number}
+ */
+ zIndexInline: 0,
+
+ /**
+ * Define where to get the original image URL for viewing.
+ * @type {string | Function}
+ */
+ url: 'src',
+
+ /**
+ * Define where to put the viewer in modal mode.
+ * @type {string | Element}
+ */
+ container: 'body',
+
+ /**
+ * Filter the images for viewing. Return true if the image is viewable.
+ * @type {Function}
+ */
+ filter: null,
+
+ /**
+ * Indicate if toggle the image size between its natural size
+ * and initial size when double click on the image or not.
+ * @type {boolean}
+ */
+ toggleOnDblclick: true,
+
+ /**
+ * Event shortcuts.
+ * @type {Function}
+ */
+ ready: null,
+ show: null,
+ shown: null,
+ hide: null,
+ hidden: null,
+ view: null,
+ viewed: null,
+ zoom: null,
+ zoomed: null
+ };
+
+ var TEMPLATE = '' + '
' + '' + '
' + '
' + '
' + '
';
+
+ var IN_BROWSER = typeof window !== 'undefined';
+ var WINDOW = IN_BROWSER ? window : {};
+ var NAMESPACE = 'viewer';
+
+ // Actions
+ var ACTION_MOVE = 'move';
+ var ACTION_SWITCH = 'switch';
+ var ACTION_ZOOM = 'zoom';
+
+ // Classes
+ var CLASS_ACTIVE = NAMESPACE + '-active';
+ var CLASS_CLOSE = NAMESPACE + '-close';
+ var CLASS_FADE = NAMESPACE + '-fade';
+ var CLASS_FIXED = NAMESPACE + '-fixed';
+ var CLASS_FULLSCREEN = NAMESPACE + '-fullscreen';
+ var CLASS_FULLSCREEN_EXIT = NAMESPACE + '-fullscreen-exit';
+ var CLASS_HIDE = NAMESPACE + '-hide';
+ var CLASS_HIDE_MD_DOWN = NAMESPACE + '-hide-md-down';
+ var CLASS_HIDE_SM_DOWN = NAMESPACE + '-hide-sm-down';
+ var CLASS_HIDE_XS_DOWN = NAMESPACE + '-hide-xs-down';
+ var CLASS_IN = NAMESPACE + '-in';
+ var CLASS_INVISIBLE = NAMESPACE + '-invisible';
+ var CLASS_LOADING = NAMESPACE + '-loading';
+ var CLASS_MOVE = NAMESPACE + '-move';
+ var CLASS_OPEN = NAMESPACE + '-open';
+ var CLASS_SHOW = NAMESPACE + '-show';
+ var CLASS_TRANSITION = NAMESPACE + '-transition';
+
+ // Events
+ var EVENT_CLICK = 'click';
+ var EVENT_DBLCLICK = 'dblclick';
+ var EVENT_DRAG_START = 'dragstart';
+ var EVENT_HIDDEN = 'hidden';
+ var EVENT_HIDE = 'hide';
+ var EVENT_KEY_DOWN = 'keydown';
+ var EVENT_LOAD = 'load';
+ var EVENT_POINTER_DOWN = WINDOW.PointerEvent ? 'pointerdown' : 'touchstart mousedown';
+ var EVENT_POINTER_MOVE = WINDOW.PointerEvent ? 'pointermove' : 'touchmove mousemove';
+ var EVENT_POINTER_UP = WINDOW.PointerEvent ? 'pointerup pointercancel' : 'touchend touchcancel mouseup';
+ var EVENT_READY = 'ready';
+ var EVENT_RESIZE = 'resize';
+ var EVENT_SHOW = 'show';
+ var EVENT_SHOWN = 'shown';
+ var EVENT_TRANSITION_END = 'transitionend';
+ var EVENT_VIEW = 'view';
+ var EVENT_VIEWED = 'viewed';
+ var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
+ var EVENT_ZOOM = 'zoom';
+ var EVENT_ZOOMED = 'zoomed';
+
+ // Data keys
+ var DATA_ACTION = NAMESPACE + 'Action';
+ var BUTTONS = ['zoom-in', 'zoom-out', 'one-to-one', 'reset', 'prev', 'play', 'next', 'rotate-left', 'rotate-right', 'flip-horizontal', 'flip-vertical'];
+
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+ };
+
+ var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+
+ var createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ /**
+ * Check if the given value is a string.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a string, else `false`.
+ */
+ function isString(value) {
+ return typeof value === 'string';
+ }
+
+ /**
+ * Check if the given value is not a number.
+ */
+ var isNaN = Number.isNaN || WINDOW.isNaN;
+
+ /**
+ * Check if the given value is a number.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a number, else `false`.
+ */
+ function isNumber(value) {
+ return typeof value === 'number' && !isNaN(value);
+ }
+
+ /**
+ * Check if the given value is undefined.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is undefined, else `false`.
+ */
+ function isUndefined(value) {
+ return typeof value === 'undefined';
+ }
+
+ /**
+ * Check if the given value is an object.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is an object, else `false`.
+ */
+ function isObject(value) {
+ return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null;
+ }
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ /**
+ * Check if the given value is a plain object.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a plain object, else `false`.
+ */
+
+ function isPlainObject(value) {
+ if (!isObject(value)) {
+ return false;
+ }
+
+ try {
+ var _constructor = value.constructor;
+ var prototype = _constructor.prototype;
+
+
+ return _constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /**
+ * Check if the given value is a function.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a function, else `false`.
+ */
+ function isFunction(value) {
+ return typeof value === 'function';
+ }
+
+ /**
+ * Iterate the given data.
+ * @param {*} data - The data to iterate.
+ * @param {Function} callback - The process function for each element.
+ * @returns {*} The original data.
+ */
+ function forEach(data, callback) {
+ if (data && isFunction(callback)) {
+ if (Array.isArray(data) || isNumber(data.length) /* array-like */) {
+ var length = data.length;
+
+ var i = void 0;
+
+ for (i = 0; i < length; i += 1) {
+ if (callback.call(data, data[i], i, data) === false) {
+ break;
+ }
+ }
+ } else if (isObject(data)) {
+ Object.keys(data).forEach(function (key) {
+ callback.call(data, data[key], key, data);
+ });
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Extend the given object.
+ * @param {*} obj - The object to be extended.
+ * @param {*} args - The rest objects which will be merged to the first object.
+ * @returns {Object} The extended object.
+ */
+ var assign = Object.assign || function assign(obj) {
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
+
+ if (isObject(obj) && args.length > 0) {
+ args.forEach(function (arg) {
+ if (isObject(arg)) {
+ Object.keys(arg).forEach(function (key) {
+ obj[key] = arg[key];
+ });
+ }
+ });
+ }
+
+ return obj;
+ };
+
+ var REGEXP_SUFFIX = /^(?:width|height|left|top|marginLeft|marginTop)$/;
+
+ /**
+ * Apply styles to the given element.
+ * @param {Element} element - The target element.
+ * @param {Object} styles - The styles for applying.
+ */
+ function setStyle(element, styles) {
+ var style = element.style;
+
+
+ forEach(styles, function (value, property) {
+ if (REGEXP_SUFFIX.test(property) && isNumber(value)) {
+ value += 'px';
+ }
+
+ style[property] = value;
+ });
+ }
+
+ /**
+ * Check if the given element has a special class.
+ * @param {Element} element - The element to check.
+ * @param {string} value - The class to search.
+ * @returns {boolean} Returns `true` if the special class was found.
+ */
+ function hasClass(element, value) {
+ return element.classList ? element.classList.contains(value) : element.className.indexOf(value) > -1;
+ }
+
+ /**
+ * Add classes to the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be added.
+ */
+ function addClass(element, value) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ forEach(element, function (elem) {
+ addClass(elem, value);
+ });
+ return;
+ }
+
+ if (element.classList) {
+ element.classList.add(value);
+ return;
+ }
+
+ var className = element.className.trim();
+
+ if (!className) {
+ element.className = value;
+ } else if (className.indexOf(value) < 0) {
+ element.className = className + ' ' + value;
+ }
+ }
+
+ /**
+ * Remove classes from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be removed.
+ */
+ function removeClass(element, value) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ forEach(element, function (elem) {
+ removeClass(elem, value);
+ });
+ return;
+ }
+
+ if (element.classList) {
+ element.classList.remove(value);
+ return;
+ }
+
+ if (element.className.indexOf(value) >= 0) {
+ element.className = element.className.replace(value, '');
+ }
+ }
+
+ /**
+ * Add or remove classes from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be toggled.
+ * @param {boolean} added - Add only.
+ */
+ function toggleClass(element, value, added) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ forEach(element, function (elem) {
+ toggleClass(elem, value, added);
+ });
+ return;
+ }
+
+ // IE10-11 doesn't support the second parameter of `classList.toggle`
+ if (added) {
+ addClass(element, value);
+ } else {
+ removeClass(element, value);
+ }
+ }
+
+ var REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g;
+
+ /**
+ * Transform the given string from camelCase to kebab-case
+ * @param {string} value - The value to transform.
+ * @returns {string} The transformed value.
+ */
+ function hyphenate(value) {
+ return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase();
+ }
+
+ /**
+ * Get data from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to get.
+ * @returns {string} The data value.
+ */
+ function getData(element, name) {
+ if (isObject(element[name])) {
+ return element[name];
+ }
+
+ if (element.dataset) {
+ return element.dataset[name];
+ }
+
+ return element.getAttribute('data-' + hyphenate(name));
+ }
+
+ /**
+ * Set data to the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to set.
+ * @param {string} data - The data value.
+ */
+ function setData(element, name, data) {
+ if (isObject(data)) {
+ element[name] = data;
+ } else if (element.dataset) {
+ element.dataset[name] = data;
+ } else {
+ element.setAttribute('data-' + hyphenate(name), data);
+ }
+ }
+
+ /**
+ * Remove data from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to remove.
+ */
+ function removeData(element, name) {
+ if (isObject(element[name])) {
+ try {
+ delete element[name];
+ } catch (e) {
+ element[name] = undefined;
+ }
+ } else if (element.dataset) {
+ // #128 Safari not allows to delete dataset property
+ try {
+ delete element.dataset[name];
+ } catch (e) {
+ element.dataset[name] = undefined;
+ }
+ } else {
+ element.removeAttribute('data-' + hyphenate(name));
+ }
+ }
+
+ var REGEXP_SPACES = /\s\s*/;
+ var onceSupported = function () {
+ var supported = false;
+
+ if (IN_BROWSER) {
+ var once = false;
+ var listener = function listener() {};
+ var options = Object.defineProperty({}, 'once', {
+ get: function get$$1() {
+ supported = true;
+ return once;
+ },
+
+
+ /**
+ * This setter can fix a `TypeError` in strict mode
+ * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only}
+ * @param {boolean} value - The value to set
+ */
+ set: function set$$1(value) {
+ once = value;
+ }
+ });
+
+ WINDOW.addEventListener('test', listener, options);
+ WINDOW.removeEventListener('test', listener, options);
+ }
+
+ return supported;
+ }();
+
+ /**
+ * Remove event listener from the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Function} listener - The event listener.
+ * @param {Object} options - The event options.
+ */
+ function removeListener(element, type, listener) {
+ var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+
+ var handler = listener;
+
+ type.trim().split(REGEXP_SPACES).forEach(function (event) {
+ if (!onceSupported) {
+ var listeners = element.listeners;
+
+
+ if (listeners && listeners[event] && listeners[event][listener]) {
+ handler = listeners[event][listener];
+ delete listeners[event][listener];
+
+ if (Object.keys(listeners[event]).length === 0) {
+ delete listeners[event];
+ }
+
+ if (Object.keys(listeners).length === 0) {
+ delete element.listeners;
+ }
+ }
+ }
+
+ element.removeEventListener(event, handler, options);
+ });
+ }
+
+ /**
+ * Add event listener to the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Function} listener - The event listener.
+ * @param {Object} options - The event options.
+ */
+ function addListener(element, type, listener) {
+ var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+
+ var _handler = listener;
+
+ type.trim().split(REGEXP_SPACES).forEach(function (event) {
+ if (options.once && !onceSupported) {
+ var _element$listeners = element.listeners,
+ listeners = _element$listeners === undefined ? {} : _element$listeners;
+
+
+ _handler = function handler() {
+ for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
+ args[_key2] = arguments[_key2];
+ }
+
+ delete listeners[event][listener];
+ element.removeEventListener(event, _handler, options);
+ listener.apply(element, args);
+ };
+
+ if (!listeners[event]) {
+ listeners[event] = {};
+ }
+
+ if (listeners[event][listener]) {
+ element.removeEventListener(event, listeners[event][listener], options);
+ }
+
+ listeners[event][listener] = _handler;
+ element.listeners = listeners;
+ }
+
+ element.addEventListener(event, _handler, options);
+ });
+ }
+
+ /**
+ * Dispatch event on the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Object} data - The additional event data.
+ * @returns {boolean} Indicate if the event is default prevented or not.
+ */
+ function dispatchEvent(element, type, data) {
+ var event = void 0;
+
+ // Event and CustomEvent on IE9-11 are global objects, not constructors
+ if (isFunction(Event) && isFunction(CustomEvent)) {
+ event = new CustomEvent(type, {
+ detail: data,
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ event = document.createEvent('CustomEvent');
+ event.initCustomEvent(type, true, true, data);
+ }
+
+ return element.dispatchEvent(event);
+ }
+
+ /**
+ * Get the offset base on the document.
+ * @param {Element} element - The target element.
+ * @returns {Object} The offset data.
+ */
+ function getOffset(element) {
+ var box = element.getBoundingClientRect();
+
+ return {
+ left: box.left + (window.pageXOffset - document.documentElement.clientLeft),
+ top: box.top + (window.pageYOffset - document.documentElement.clientTop)
+ };
+ }
+
+ /**
+ * Get transforms base on the given object.
+ * @param {Object} obj - The target object.
+ * @returns {string} A string contains transform values.
+ */
+ function getTransforms(_ref) {
+ var rotate = _ref.rotate,
+ scaleX = _ref.scaleX,
+ scaleY = _ref.scaleY,
+ translateX = _ref.translateX,
+ translateY = _ref.translateY;
+
+ var values = [];
+
+ if (isNumber(translateX) && translateX !== 0) {
+ values.push('translateX(' + translateX + 'px)');
+ }
+
+ if (isNumber(translateY) && translateY !== 0) {
+ values.push('translateY(' + translateY + 'px)');
+ }
+
+ // Rotate should come first before scale to match orientation transform
+ if (isNumber(rotate) && rotate !== 0) {
+ values.push('rotate(' + rotate + 'deg)');
+ }
+
+ if (isNumber(scaleX) && scaleX !== 1) {
+ values.push('scaleX(' + scaleX + ')');
+ }
+
+ if (isNumber(scaleY) && scaleY !== 1) {
+ values.push('scaleY(' + scaleY + ')');
+ }
+
+ var transform = values.length ? values.join(' ') : 'none';
+
+ return {
+ WebkitTransform: transform,
+ msTransform: transform,
+ transform: transform
+ };
+ }
+
+ /**
+ * Get an image name from an image url.
+ * @param {string} url - The target url.
+ * @example
+ * // picture.jpg
+ * getImageNameFromURL('http://domain.com/path/to/picture.jpg?size=1280×960')
+ * @returns {string} A string contains the image name.
+ */
+ function getImageNameFromURL(url) {
+ return isString(url) ? url.replace(/^.*\//, '').replace(/[?].*$/, '') : '';
+ }
+
+ var IS_SAFARI = WINDOW.navigator && /(Macintosh|iPhone|iPod|iPad).*AppleWebKit/i.test(WINDOW.navigator.userAgent);
+
+ /**
+ * Get an image's natural sizes.
+ * @param {string} image - The target image.
+ * @param {Function} callback - The callback function.
+ * @returns {HTMLImageElement} The new image.
+ */
+ function getImageNaturalSizes(image, callback) {
+ var newImage = document.createElement('img');
+
+ // Modern browsers (except Safari)
+ if (image.naturalWidth && !IS_SAFARI) {
+ callback(image.naturalWidth, image.naturalHeight);
+ return newImage;
+ }
+
+ var body = document.body || document.documentElement;
+
+ newImage.onload = function () {
+ callback(newImage.width, newImage.height);
+
+ if (!IS_SAFARI) {
+ body.removeChild(newImage);
+ }
+ };
+
+ newImage.src = image.src;
+
+ // iOS Safari will convert the image automatically
+ // with its orientation once append it into DOM
+ if (!IS_SAFARI) {
+ newImage.style.cssText = 'left:0;' + 'max-height:none!important;' + 'max-width:none!important;' + 'min-height:0!important;' + 'min-width:0!important;' + 'opacity:0;' + 'position:absolute;' + 'top:0;' + 'z-index:-1;';
+ body.appendChild(newImage);
+ }
+
+ return newImage;
+ }
+
+ /**
+ * Get the related class name of a responsive type number.
+ * @param {string} type - The responsive type.
+ * @returns {string} The related class name.
+ */
+ function getResponsiveClass(type) {
+ switch (type) {
+ case 2:
+ return CLASS_HIDE_XS_DOWN;
+
+ case 3:
+ return CLASS_HIDE_SM_DOWN;
+
+ case 4:
+ return CLASS_HIDE_MD_DOWN;
+
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Get the max ratio of a group of pointers.
+ * @param {string} pointers - The target pointers.
+ * @returns {number} The result ratio.
+ */
+ function getMaxZoomRatio(pointers) {
+ var pointers2 = assign({}, pointers);
+ var ratios = [];
+
+ forEach(pointers, function (pointer, pointerId) {
+ delete pointers2[pointerId];
+
+ forEach(pointers2, function (pointer2) {
+ var x1 = Math.abs(pointer.startX - pointer2.startX);
+ var y1 = Math.abs(pointer.startY - pointer2.startY);
+ var x2 = Math.abs(pointer.endX - pointer2.endX);
+ var y2 = Math.abs(pointer.endY - pointer2.endY);
+ var z1 = Math.sqrt(x1 * x1 + y1 * y1);
+ var z2 = Math.sqrt(x2 * x2 + y2 * y2);
+ var ratio = (z2 - z1) / z1;
+
+ ratios.push(ratio);
+ });
+ });
+
+ ratios.sort(function (a, b) {
+ return Math.abs(a) < Math.abs(b);
+ });
+
+ return ratios[0];
+ }
+
+ /**
+ * Get a pointer from an event object.
+ * @param {Object} event - The target event object.
+ * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not.
+ * @returns {Object} The result pointer contains start and/or end point coordinates.
+ */
+ function getPointer(_ref2, endOnly) {
+ var pageX = _ref2.pageX,
+ pageY = _ref2.pageY;
+
+ var end = {
+ endX: pageX,
+ endY: pageY
+ };
+
+ return endOnly ? end : assign({
+ startX: pageX,
+ startY: pageY
+ }, end);
+ }
+
+ /**
+ * Get the center point coordinate of a group of pointers.
+ * @param {Object} pointers - The target pointers.
+ * @returns {Object} The center point coordinate.
+ */
+ function getPointersCenter(pointers) {
+ var pageX = 0;
+ var pageY = 0;
+ var count = 0;
+
+ forEach(pointers, function (_ref3) {
+ var startX = _ref3.startX,
+ startY = _ref3.startY;
+
+ pageX += startX;
+ pageY += startY;
+ count += 1;
+ });
+
+ pageX /= count;
+ pageY /= count;
+
+ return {
+ pageX: pageX,
+ pageY: pageY
+ };
+ }
+
+ var render = {
+ render: function render() {
+ this.initContainer();
+ this.initViewer();
+ this.initList();
+ this.renderViewer();
+ },
+ initContainer: function initContainer() {
+ this.containerData = {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+ },
+ initViewer: function initViewer() {
+ var options = this.options,
+ parent = this.parent;
+
+ var viewerData = void 0;
+
+ if (options.inline) {
+ viewerData = {
+ width: Math.max(parent.offsetWidth, options.minWidth),
+ height: Math.max(parent.offsetHeight, options.minHeight)
+ };
+
+ this.parentData = viewerData;
+ }
+
+ if (this.fulled || !viewerData) {
+ viewerData = this.containerData;
+ }
+
+ this.viewerData = assign({}, viewerData);
+ },
+ renderViewer: function renderViewer() {
+ if (this.options.inline && !this.fulled) {
+ setStyle(this.viewer, this.viewerData);
+ }
+ },
+ initList: function initList() {
+ var _this = this;
+
+ var element = this.element,
+ options = this.options,
+ list = this.list;
+
+ var items = [];
+
+ forEach(this.images, function (image, i) {
+ var src = image.src;
+
+ var alt = image.alt || getImageNameFromURL(src);
+ var url = options.url;
+
+
+ if (isString(url)) {
+ url = image.getAttribute(url);
+ } else if (isFunction(url)) {
+ url = url.call(_this, image);
+ }
+
+ if (src || url) {
+ items.push('' + ' ' + ' ');
+ }
+ });
+
+ list.innerHTML = items.join('');
+ this.items = list.getElementsByTagName('li');
+ forEach(this.items, function (item) {
+ var image = item.firstElementChild;
+
+ setData(image, 'filled', true);
+
+ if (options.loading) {
+ addClass(item, CLASS_LOADING);
+ }
+
+ addListener(image, EVENT_LOAD, function (event) {
+ if (options.loading) {
+ removeClass(item, CLASS_LOADING);
+ }
+
+ _this.loadImage(event);
+ }, {
+ once: true
+ });
+ });
+
+ if (options.transition) {
+ addListener(element, EVENT_VIEWED, function () {
+ addClass(list, CLASS_TRANSITION);
+ }, {
+ once: true
+ });
+ }
+ },
+ renderList: function renderList(index) {
+ var i = index || this.index;
+ var width = this.items[i].offsetWidth || 30;
+ var outerWidth = width + 1; // 1 pixel of `margin-left` width
+
+ // Place the active item in the center of the screen
+ setStyle(this.list, assign({
+ width: outerWidth * this.length
+ }, getTransforms({
+ translateX: (this.viewerData.width - width) / 2 - outerWidth * i
+ })));
+ },
+ resetList: function resetList() {
+ var list = this.list;
+
+
+ list.innerHTML = '';
+ removeClass(list, CLASS_TRANSITION);
+ setStyle(list, getTransforms({
+ translateX: 0
+ }));
+ },
+ initImage: function initImage(done) {
+ var _this2 = this;
+
+ var options = this.options,
+ image = this.image,
+ viewerData = this.viewerData;
+
+ var footerHeight = this.footer.offsetHeight;
+ var viewerWidth = viewerData.width;
+ var viewerHeight = Math.max(viewerData.height - footerHeight, footerHeight);
+ var oldImageData = this.imageData || {};
+ var sizingImage = void 0;
+
+ this.imageInitializing = {
+ abort: function abort() {
+ sizingImage.onload = null;
+ }
+ };
+
+ sizingImage = getImageNaturalSizes(image, function (naturalWidth, naturalHeight) {
+ var aspectRatio = naturalWidth / naturalHeight;
+ var width = viewerWidth;
+ var height = viewerHeight;
+
+ _this2.imageInitializing = false;
+
+ if (viewerHeight * aspectRatio > viewerWidth) {
+ height = viewerWidth / aspectRatio;
+ } else {
+ width = viewerHeight * aspectRatio;
+ }
+
+ width = Math.min(width * 0.9, naturalWidth);
+ height = Math.min(height * 0.9, naturalHeight);
+
+ var imageData = {
+ naturalWidth: naturalWidth,
+ naturalHeight: naturalHeight,
+ aspectRatio: aspectRatio,
+ ratio: width / naturalWidth,
+ width: width,
+ height: height,
+ left: (viewerWidth - width) / 2,
+ top: (viewerHeight - height) / 2
+ };
+ var initialImageData = assign({}, imageData);
+
+ if (options.rotatable) {
+ imageData.rotate = oldImageData.rotate || 0;
+ initialImageData.rotate = 0;
+ }
+
+ if (options.scalable) {
+ imageData.scaleX = oldImageData.scaleX || 1;
+ imageData.scaleY = oldImageData.scaleY || 1;
+ initialImageData.scaleX = 1;
+ initialImageData.scaleY = 1;
+ }
+
+ _this2.imageData = imageData;
+ _this2.initialImageData = initialImageData;
+
+ if (done) {
+ done();
+ }
+ });
+ },
+ renderImage: function renderImage(done) {
+ var _this3 = this;
+
+ var image = this.image,
+ imageData = this.imageData;
+
+
+ setStyle(image, assign({
+ width: imageData.width,
+ height: imageData.height,
+ marginLeft: imageData.left,
+ marginTop: imageData.top
+ }, getTransforms(imageData)));
+
+ if (done) {
+ if ((this.viewing || this.zooming) && this.options.transition) {
+ var onTransitionEnd = function onTransitionEnd() {
+ _this3.imageRendering = false;
+ done();
+ };
+
+ this.imageRendering = {
+ abort: function abort() {
+ removeListener(image, EVENT_TRANSITION_END, onTransitionEnd);
+ }
+ };
+
+ addListener(image, EVENT_TRANSITION_END, onTransitionEnd, {
+ once: true
+ });
+ } else {
+ done();
+ }
+ }
+ },
+ resetImage: function resetImage() {
+ // this.image only defined after viewed
+ if (this.viewing || this.viewed) {
+ var image = this.image;
+
+
+ if (this.viewing) {
+ this.viewing.abort();
+ }
+
+ image.parentNode.removeChild(image);
+ this.image = null;
+ }
+ }
+ };
+
+ var events = {
+ bind: function bind() {
+ var canvas = this.canvas,
+ element = this.element,
+ viewer = this.viewer;
+
+
+ addListener(viewer, EVENT_CLICK, this.onClick = this.click.bind(this));
+ addListener(viewer, EVENT_WHEEL, this.onWheel = this.wheel.bind(this));
+ addListener(viewer, EVENT_DRAG_START, this.onDragStart = this.dragstart.bind(this));
+
+ if (this.options.toggleOnDblclick) {
+ addListener(canvas, EVENT_DBLCLICK, this.onDblclick = this.dblclick.bind(this));
+ }
+
+ addListener(canvas, EVENT_POINTER_DOWN, this.onPointerDown = this.pointerdown.bind(this));
+ addListener(element.ownerDocument, EVENT_POINTER_MOVE, this.onPointerMove = this.pointermove.bind(this));
+ addListener(element.ownerDocument, EVENT_POINTER_UP, this.onPointerUp = this.pointerup.bind(this));
+ addListener(element.ownerDocument, EVENT_KEY_DOWN, this.onKeyDown = this.keydown.bind(this));
+ addListener(window, EVENT_RESIZE, this.onResize = this.resize.bind(this));
+ },
+ unbind: function unbind() {
+ var canvas = this.canvas,
+ element = this.element,
+ viewer = this.viewer;
+
+
+ removeListener(viewer, EVENT_CLICK, this.onClick);
+ removeListener(viewer, EVENT_WHEEL, this.onWheel);
+ removeListener(viewer, EVENT_DRAG_START, this.onDragStart);
+
+ if (this.options.toggleOnDblclick) {
+ removeListener(canvas, EVENT_DBLCLICK, this.onDblclick);
+ }
+
+ removeListener(canvas, EVENT_POINTER_DOWN, this.onPointerDown);
+ removeListener(element.ownerDocument, EVENT_POINTER_MOVE, this.onPointerMove);
+ removeListener(element.ownerDocument, EVENT_POINTER_UP, this.onPointerUp);
+ removeListener(element.ownerDocument, EVENT_KEY_DOWN, this.onKeyDown);
+ removeListener(window, EVENT_RESIZE, this.onResize);
+ }
+ };
+
+ var handlers = {
+ click: function click(_ref) {
+ var target = _ref.target;
+ var options = this.options,
+ imageData = this.imageData;
+
+ var action = getData(target, DATA_ACTION);
+
+ switch (action) {
+ case 'mix':
+ if (this.played) {
+ this.stop();
+ } else if (options.inline) {
+ if (this.fulled) {
+ this.exit();
+ } else {
+ this.full();
+ }
+ } else {
+ this.hide();
+ }
+
+ break;
+
+ case 'hide':
+ this.hide();
+ break;
+
+ case 'view':
+ this.view(getData(target, 'index'));
+ break;
+
+ case 'zoom-in':
+ this.zoom(0.1, true);
+ break;
+
+ case 'zoom-out':
+ this.zoom(-0.1, true);
+ break;
+
+ case 'one-to-one':
+ this.toggle();
+ break;
+
+ case 'reset':
+ this.reset();
+ break;
+
+ case 'prev':
+ this.prev(options.loop);
+ break;
+
+ case 'play':
+ this.play(options.fullscreen);
+ break;
+
+ case 'next':
+ this.next(options.loop);
+ break;
+
+ case 'rotate-left':
+ this.rotate(-90);
+ break;
+
+ case 'rotate-right':
+ this.rotate(90);
+ break;
+
+ case 'flip-horizontal':
+ this.scaleX(-imageData.scaleX || -1);
+ break;
+
+ case 'flip-vertical':
+ this.scaleY(-imageData.scaleY || -1);
+ break;
+
+ default:
+ if (this.played) {
+ this.stop();
+ }
+ }
+ },
+ dblclick: function dblclick(event) {
+ if (event.target.parentElement === this.canvas) {
+ this.toggle();
+ }
+ },
+ load: function load() {
+ var _this = this;
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ this.timeout = false;
+ }
+
+ var element = this.element,
+ options = this.options,
+ image = this.image,
+ index = this.index,
+ viewerData = this.viewerData;
+
+
+ removeClass(image, CLASS_INVISIBLE);
+
+ if (options.loading) {
+ removeClass(this.canvas, CLASS_LOADING);
+ }
+
+ image.style.cssText = 'height:0;' + ('margin-left:' + viewerData.width / 2 + 'px;') + ('margin-top:' + viewerData.height / 2 + 'px;') + 'max-width:none!important;' + 'position:absolute;' + 'width:0;';
+
+ this.initImage(function () {
+ toggleClass(image, CLASS_MOVE, options.movable);
+ toggleClass(image, CLASS_TRANSITION, options.transition);
+
+ _this.renderImage(function () {
+ _this.viewed = true;
+ _this.viewing = false;
+
+ if (isFunction(options.viewed)) {
+ addListener(element, EVENT_VIEWED, options.viewed, {
+ once: true
+ });
+ }
+
+ dispatchEvent(element, EVENT_VIEWED, {
+ originalImage: _this.images[index],
+ index: index,
+ image: image
+ });
+ });
+ });
+ },
+ loadImage: function loadImage(e) {
+ var image = e.target;
+ var parent = image.parentNode;
+ var parentWidth = parent.offsetWidth || 30;
+ var parentHeight = parent.offsetHeight || 50;
+ var filled = !!getData(image, 'filled');
+
+ getImageNaturalSizes(image, function (naturalWidth, naturalHeight) {
+ var aspectRatio = naturalWidth / naturalHeight;
+ var width = parentWidth;
+ var height = parentHeight;
+
+ if (parentHeight * aspectRatio > parentWidth) {
+ if (filled) {
+ width = parentHeight * aspectRatio;
+ } else {
+ height = parentWidth / aspectRatio;
+ }
+ } else if (filled) {
+ height = parentWidth / aspectRatio;
+ } else {
+ width = parentHeight * aspectRatio;
+ }
+
+ setStyle(image, assign({
+ width: width,
+ height: height
+ }, getTransforms({
+ translateX: (parentWidth - width) / 2,
+ translateY: (parentHeight - height) / 2
+ })));
+ });
+ },
+ keydown: function keydown(e) {
+ var options = this.options;
+
+
+ if (!this.fulled || !options.keyboard) {
+ return;
+ }
+
+ switch (e.keyCode || e.which || e.charCode) {
+ // Escape
+ case 27:
+ if (this.played) {
+ this.stop();
+ } else if (options.inline) {
+ if (this.fulled) {
+ this.exit();
+ }
+ } else {
+ this.hide();
+ }
+
+ break;
+
+ // Space
+ case 32:
+ if (this.played) {
+ this.stop();
+ }
+
+ break;
+
+ // ArrowLeft
+ case 37:
+ this.prev(options.loop);
+ break;
+
+ // ArrowUp
+ case 38:
+ // Prevent scroll on Firefox
+ e.preventDefault();
+
+ // Zoom in
+ this.zoom(options.zoomRatio, true);
+ break;
+
+ // ArrowRight
+ case 39:
+ this.next(options.loop);
+ break;
+
+ // ArrowDown
+ case 40:
+ // Prevent scroll on Firefox
+ e.preventDefault();
+
+ // Zoom out
+ this.zoom(-options.zoomRatio, true);
+ break;
+
+ // Ctrl + 0
+ case 48:
+ // Fall through
+
+ // Ctrl + 1
+ // eslint-disable-next-line no-fallthrough
+ case 49:
+ if (e.ctrlKey) {
+ e.preventDefault();
+ this.toggle();
+ }
+
+ break;
+
+ default:
+ }
+ },
+ dragstart: function dragstart(e) {
+ if (e.target.tagName.toLowerCase() === 'img') {
+ e.preventDefault();
+ }
+ },
+ pointerdown: function pointerdown(e) {
+ var options = this.options,
+ pointers = this.pointers;
+
+
+ if (!this.viewed || this.showing || this.viewing || this.hiding) {
+ return;
+ }
+
+ // This line is required for preventing page zooming in iOS browsers
+ e.preventDefault();
+
+ if (e.changedTouches) {
+ forEach(e.changedTouches, function (touch) {
+ pointers[touch.identifier] = getPointer(touch);
+ });
+ } else {
+ pointers[e.pointerId || 0] = getPointer(e);
+ }
+
+ var action = options.movable ? ACTION_MOVE : false;
+
+ if (Object.keys(pointers).length > 1) {
+ action = ACTION_ZOOM;
+ } else if ((e.pointerType === 'touch' || e.type === 'touchstart') && this.isSwitchable()) {
+ action = ACTION_SWITCH;
+ }
+
+ if (options.transition && (action === ACTION_MOVE || action === ACTION_ZOOM)) {
+ removeClass(this.image, CLASS_TRANSITION);
+ }
+
+ this.action = action;
+ },
+ pointermove: function pointermove(e) {
+ var pointers = this.pointers,
+ action = this.action;
+
+
+ if (!this.viewed || !action) {
+ return;
+ }
+
+ e.preventDefault();
+
+ if (e.changedTouches) {
+ forEach(e.changedTouches, function (touch) {
+ assign(pointers[touch.identifier], getPointer(touch, true));
+ });
+ } else {
+ assign(pointers[e.pointerId || 0], getPointer(e, true));
+ }
+
+ this.change(e);
+ },
+ pointerup: function pointerup(e) {
+ var action = this.action,
+ pointers = this.pointers;
+
+
+ if (e.changedTouches) {
+ forEach(e.changedTouches, function (touch) {
+ delete pointers[touch.identifier];
+ });
+ } else {
+ delete pointers[e.pointerId || 0];
+ }
+
+ if (!action) {
+ return;
+ }
+
+ e.preventDefault();
+
+ if (this.options.transition && (action === ACTION_MOVE || action === ACTION_ZOOM)) {
+ addClass(this.image, CLASS_TRANSITION);
+ }
+
+ this.action = false;
+ },
+ resize: function resize() {
+ var _this2 = this;
+
+ if (!this.isShown || this.hiding) {
+ return;
+ }
+
+ this.initContainer();
+ this.initViewer();
+ this.renderViewer();
+ this.renderList();
+
+ if (this.viewed) {
+ this.initImage(function () {
+ _this2.renderImage();
+ });
+ }
+
+ if (this.played) {
+ if (this.options.fullscreen && this.fulled && !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
+ this.stop();
+ return;
+ }
+
+ forEach(this.player.getElementsByTagName('img'), function (image) {
+ addListener(image, EVENT_LOAD, _this2.loadImage.bind(_this2), {
+ once: true
+ });
+ dispatchEvent(image, EVENT_LOAD);
+ });
+ }
+ },
+ wheel: function wheel(e) {
+ var _this3 = this;
+
+ if (!this.viewed) {
+ return;
+ }
+
+ e.preventDefault();
+
+ // Limit wheel speed to prevent zoom too fast
+ if (this.wheeling) {
+ return;
+ }
+
+ this.wheeling = true;
+
+ setTimeout(function () {
+ _this3.wheeling = false;
+ }, 50);
+
+ var ratio = Number(this.options.zoomRatio) || 0.1;
+ var delta = 1;
+
+ if (e.deltaY) {
+ delta = e.deltaY > 0 ? 1 : -1;
+ } else if (e.wheelDelta) {
+ delta = -e.wheelDelta / 120;
+ } else if (e.detail) {
+ delta = e.detail > 0 ? 1 : -1;
+ }
+
+ this.zoom(-delta * ratio, true, e);
+ }
+ };
+
+ var methods = {
+ /** Show the viewer (only available in modal mode)
+ * @param {boolean} [immediate=false] - Indicates if show the viewer immediately or not.
+ * @returns {Viewer} this
+ */
+ show: function show() {
+ var immediate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var element = this.element,
+ options = this.options;
+
+
+ if (options.inline || this.showing || this.isShown || this.showing) {
+ return this;
+ }
+
+ if (!this.ready) {
+ this.build();
+
+ if (this.ready) {
+ this.show(immediate);
+ }
+
+ return this;
+ }
+
+ if (isFunction(options.show)) {
+ addListener(element, EVENT_SHOW, options.show, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_SHOW) === false || !this.ready) {
+ return this;
+ }
+
+ if (this.hiding) {
+ this.transitioning.abort();
+ }
+
+ this.showing = true;
+ this.open();
+
+ var viewer = this.viewer;
+
+
+ removeClass(viewer, CLASS_HIDE);
+
+ if (options.transition && !immediate) {
+ var shown = this.shown.bind(this);
+
+ this.transitioning = {
+ abort: function abort() {
+ removeListener(viewer, EVENT_TRANSITION_END, shown);
+ removeClass(viewer, CLASS_IN);
+ }
+ };
+
+ addClass(viewer, CLASS_TRANSITION);
+
+ // Force reflow to enable CSS3 transition
+ // eslint-disable-next-line
+ viewer.offsetWidth;
+ addListener(viewer, EVENT_TRANSITION_END, shown, {
+ once: true
+ });
+ addClass(viewer, CLASS_IN);
+ } else {
+ addClass(viewer, CLASS_IN);
+ this.shown();
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Hide the viewer (only available in modal mode)
+ * @param {boolean} [immediate=false] - Indicates if hide the viewer immediately or not.
+ * @returns {Viewer} this
+ */
+ hide: function hide() {
+ var immediate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var element = this.element,
+ options = this.options;
+
+
+ if (options.inline || this.hiding || !(this.isShown || this.showing)) {
+ return this;
+ }
+
+ if (isFunction(options.hide)) {
+ addListener(element, EVENT_HIDE, options.hide, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_HIDE) === false) {
+ return this;
+ }
+
+ if (this.showing) {
+ this.transitioning.abort();
+ }
+
+ this.hiding = true;
+
+ if (this.played) {
+ this.stop();
+ } else if (this.viewing) {
+ this.viewing.abort();
+ }
+
+ var viewer = this.viewer;
+
+
+ if (options.transition && !immediate) {
+ var hidden = this.hidden.bind(this);
+ var hide = function hide() {
+ addListener(viewer, EVENT_TRANSITION_END, hidden, {
+ once: true
+ });
+ removeClass(viewer, CLASS_IN);
+ };
+
+ this.transitioning = {
+ abort: function abort() {
+ if (this.viewed) {
+ removeListener(this.image, EVENT_TRANSITION_END, hide);
+ } else {
+ removeListener(viewer, EVENT_TRANSITION_END, hidden);
+ }
+ }
+ };
+
+ if (this.viewed) {
+ addListener(this.image, EVENT_TRANSITION_END, hide, {
+ once: true
+ });
+ this.zoomTo(0, false, false, true);
+ } else {
+ hide();
+ }
+ } else {
+ removeClass(viewer, CLASS_IN);
+ this.hidden();
+ }
+
+ return this;
+ },
+
+
+ /**
+ * View one of the images with image's index
+ * @param {number} index - The index of the image to view.
+ * @returns {Viewer} this
+ */
+ view: function view() {
+ var _this = this;
+
+ var index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.options.initialViewIndex;
+
+ index = Number(index) || 0;
+
+ if (!this.isShown) {
+ this.index = index;
+ return this.show();
+ }
+
+ if (this.hiding || this.played || index < 0 || index >= this.length || this.viewed && index === this.index) {
+ return this;
+ }
+
+ if (this.viewing) {
+ this.viewing.abort();
+ }
+
+ var element = this.element,
+ options = this.options,
+ title = this.title,
+ canvas = this.canvas;
+
+ var item = this.items[index];
+ var img = item.querySelector('img');
+ var url = getData(img, 'originalUrl');
+ var alt = img.getAttribute('alt');
+ var image = document.createElement('img');
+
+ image.src = url;
+ image.alt = alt;
+
+ if (isFunction(options.view)) {
+ addListener(element, EVENT_VIEW, options.view, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_VIEW, {
+ originalImage: this.images[index],
+ index: index,
+ image: image
+ }) === false || !this.isShown || this.hiding || this.played) {
+ return this;
+ }
+
+ this.image = image;
+ removeClass(this.items[this.index], CLASS_ACTIVE);
+ addClass(item, CLASS_ACTIVE);
+ this.viewed = false;
+ this.index = index;
+ this.imageData = {};
+ addClass(image, CLASS_INVISIBLE);
+
+ if (options.loading) {
+ addClass(canvas, CLASS_LOADING);
+ }
+
+ canvas.innerHTML = '';
+ canvas.appendChild(image);
+
+ // Center current item
+ this.renderList();
+
+ // Clear title
+ title.innerHTML = '';
+
+ // Generate title after viewed
+ var onViewed = function onViewed() {
+ var imageData = _this.imageData;
+
+ var render = Array.isArray(options.title) ? options.title[1] : options.title;
+
+ title.innerHTML = isFunction(render) ? render.call(_this, image, imageData) : alt + ' (' + imageData.naturalWidth + ' \xD7 ' + imageData.naturalHeight + ')';
+ };
+ var onLoad = void 0;
+
+ addListener(element, EVENT_VIEWED, onViewed, {
+ once: true
+ });
+
+ this.viewing = {
+ abort: function abort() {
+ removeListener(element, EVENT_VIEWED, onViewed);
+
+ if (image.complete) {
+ if (this.imageRendering) {
+ this.imageRendering.abort();
+ } else if (this.imageInitializing) {
+ this.imageInitializing.abort();
+ }
+ } else {
+ removeListener(image, EVENT_LOAD, onLoad);
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ }
+ }
+ };
+
+ if (image.complete) {
+ this.load();
+ } else {
+ addListener(image, EVENT_LOAD, onLoad = this.load.bind(this), {
+ once: true
+ });
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ // Make the image visible if it fails to load within 1s
+ this.timeout = setTimeout(function () {
+ removeClass(image, CLASS_INVISIBLE);
+ _this.timeout = false;
+ }, 1000);
+ }
+
+ return this;
+ },
+
+
+ /**
+ * View the previous image
+ * @param {boolean} [loop=false] - Indicate if view the last one
+ * when it is the first one at present.
+ * @returns {Viewer} this
+ */
+ prev: function prev() {
+ var loop = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ var index = this.index - 1;
+
+ if (index < 0) {
+ index = loop ? this.length - 1 : 0;
+ }
+
+ this.view(index);
+ return this;
+ },
+
+
+ /**
+ * View the next image
+ * @param {boolean} [loop=false] - Indicate if view the first one
+ * when it is the last one at present.
+ * @returns {Viewer} this
+ */
+ next: function next() {
+ var loop = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ var maxIndex = this.length - 1;
+ var index = this.index + 1;
+
+ if (index > maxIndex) {
+ index = loop ? 0 : maxIndex;
+ }
+
+ this.view(index);
+ return this;
+ },
+
+
+ /**
+ * Move the image with relative offsets.
+ * @param {number} offsetX - The relative offset distance on the x-axis.
+ * @param {number} offsetY - The relative offset distance on the y-axis.
+ * @returns {Viewer} this
+ */
+ move: function move(offsetX, offsetY) {
+ var imageData = this.imageData;
+
+
+ this.moveTo(isUndefined(offsetX) ? offsetX : imageData.left + Number(offsetX), isUndefined(offsetY) ? offsetY : imageData.top + Number(offsetY));
+
+ return this;
+ },
+
+
+ /**
+ * Move the image to an absolute point.
+ * @param {number} x - The x-axis coordinate.
+ * @param {number} [y=x] - The y-axis coordinate.
+ * @returns {Viewer} this
+ */
+ moveTo: function moveTo(x) {
+ var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : x;
+ var imageData = this.imageData;
+
+
+ x = Number(x);
+ y = Number(y);
+
+ if (this.viewed && !this.played && this.options.movable) {
+ var changed = false;
+
+ if (isNumber(x)) {
+ imageData.left = x;
+ changed = true;
+ }
+
+ if (isNumber(y)) {
+ imageData.top = y;
+ changed = true;
+ }
+
+ if (changed) {
+ this.renderImage();
+ }
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Zoom the image with a relative ratio.
+ * @param {number} ratio - The target ratio.
+ * @param {boolean} [hasTooltip=false] - Indicates if it has a tooltip or not.
+ * @param {Event} [_originalEvent=null] - The original event if any.
+ * @returns {Viewer} this
+ */
+ zoom: function zoom(ratio) {
+ var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+ var _originalEvent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+
+ var imageData = this.imageData;
+
+
+ ratio = Number(ratio);
+
+ if (ratio < 0) {
+ ratio = 1 / (1 - ratio);
+ } else {
+ ratio = 1 + ratio;
+ }
+
+ this.zoomTo(imageData.width * ratio / imageData.naturalWidth, hasTooltip, _originalEvent);
+
+ return this;
+ },
+
+
+ /**
+ * Zoom the image to an absolute ratio.
+ * @param {number} ratio - The target ratio.
+ * @param {boolean} [hasTooltip=false] - Indicates if it has a tooltip or not.
+ * @param {Event} [_originalEvent=null] - The original event if any.
+ * @param {Event} [_zoomable=false] - Indicates if the current zoom is available or not.
+ * @returns {Viewer} this
+ */
+ zoomTo: function zoomTo(ratio) {
+ var hasTooltip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+
+ var _this2 = this;
+
+ var _originalEvent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+
+ var _zoomable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+
+ var element = this.element,
+ options = this.options,
+ pointers = this.pointers,
+ imageData = this.imageData;
+
+
+ ratio = Math.max(0, ratio);
+
+ if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) {
+ if (!_zoomable) {
+ var minZoomRatio = Math.max(0.01, options.minZoomRatio);
+ var maxZoomRatio = Math.min(100, options.maxZoomRatio);
+
+ ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio);
+ }
+
+ if (_originalEvent && ratio > 0.95 && ratio < 1.05) {
+ ratio = 1;
+ }
+
+ var newWidth = imageData.naturalWidth * ratio;
+ var newHeight = imageData.naturalHeight * ratio;
+ var oldRatio = imageData.width / imageData.naturalWidth;
+
+ if (isFunction(options.zoom)) {
+ addListener(element, EVENT_ZOOM, options.zoom, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_ZOOM, {
+ ratio: ratio,
+ oldRatio: oldRatio,
+ originalEvent: _originalEvent
+ }) === false) {
+ return this;
+ }
+
+ this.zooming = true;
+
+ if (_originalEvent) {
+ var offset = getOffset(this.viewer);
+ var center = pointers && Object.keys(pointers).length ? getPointersCenter(pointers) : {
+ pageX: _originalEvent.pageX,
+ pageY: _originalEvent.pageY
+ };
+
+ // Zoom from the triggering point of the event
+ imageData.left -= (newWidth - imageData.width) * ((center.pageX - offset.left - imageData.left) / imageData.width);
+ imageData.top -= (newHeight - imageData.height) * ((center.pageY - offset.top - imageData.top) / imageData.height);
+ } else {
+ // Zoom from the center of the image
+ imageData.left -= (newWidth - imageData.width) / 2;
+ imageData.top -= (newHeight - imageData.height) / 2;
+ }
+
+ imageData.width = newWidth;
+ imageData.height = newHeight;
+ imageData.ratio = ratio;
+ this.renderImage(function () {
+ _this2.zooming = false;
+
+ if (isFunction(options.zoomed)) {
+ addListener(element, EVENT_ZOOMED, options.zoomed, {
+ once: true
+ });
+ }
+
+ dispatchEvent(element, EVENT_ZOOMED, {
+ ratio: ratio,
+ oldRatio: oldRatio,
+ originalEvent: _originalEvent
+ });
+ });
+
+ if (hasTooltip) {
+ this.tooltip();
+ }
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Rotate the image with a relative degree.
+ * @param {number} degree - The rotate degree.
+ * @returns {Viewer} this
+ */
+ rotate: function rotate(degree) {
+ this.rotateTo((this.imageData.rotate || 0) + Number(degree));
+
+ return this;
+ },
+
+
+ /**
+ * Rotate the image to an absolute degree.
+ * @param {number} degree - The rotate degree.
+ * @returns {Viewer} this
+ */
+ rotateTo: function rotateTo(degree) {
+ var imageData = this.imageData;
+
+
+ degree = Number(degree);
+
+ if (isNumber(degree) && this.viewed && !this.played && this.options.rotatable) {
+ imageData.rotate = degree;
+ this.renderImage();
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Scale the image on the x-axis.
+ * @param {number} scaleX - The scale ratio on the x-axis.
+ * @returns {Viewer} this
+ */
+ scaleX: function scaleX(_scaleX) {
+ this.scale(_scaleX, this.imageData.scaleY);
+
+ return this;
+ },
+
+
+ /**
+ * Scale the image on the y-axis.
+ * @param {number} scaleY - The scale ratio on the y-axis.
+ * @returns {Viewer} this
+ */
+ scaleY: function scaleY(_scaleY) {
+ this.scale(this.imageData.scaleX, _scaleY);
+
+ return this;
+ },
+
+
+ /**
+ * Scale the image.
+ * @param {number} scaleX - The scale ratio on the x-axis.
+ * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.
+ * @returns {Viewer} this
+ */
+ scale: function scale(scaleX) {
+ var scaleY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : scaleX;
+ var imageData = this.imageData;
+
+
+ scaleX = Number(scaleX);
+ scaleY = Number(scaleY);
+
+ if (this.viewed && !this.played && this.options.scalable) {
+ var changed = false;
+
+ if (isNumber(scaleX)) {
+ imageData.scaleX = scaleX;
+ changed = true;
+ }
+
+ if (isNumber(scaleY)) {
+ imageData.scaleY = scaleY;
+ changed = true;
+ }
+
+ if (changed) {
+ this.renderImage();
+ }
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Play the images
+ * @param {boolean} [fullscreen=false] - Indicate if request fullscreen or not.
+ * @returns {Viewer} this
+ */
+ play: function play() {
+ var _this3 = this;
+
+ var fullscreen = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (!this.isShown || this.played) {
+ return this;
+ }
+
+ var options = this.options,
+ player = this.player;
+
+ var onLoad = this.loadImage.bind(this);
+ var list = [];
+ var total = 0;
+ var index = 0;
+
+ this.played = true;
+ this.onLoadWhenPlay = onLoad;
+
+ if (fullscreen) {
+ this.requestFullscreen();
+ }
+
+ addClass(player, CLASS_SHOW);
+ forEach(this.items, function (item, i) {
+ var img = item.querySelector('img');
+ var image = document.createElement('img');
+
+ image.src = getData(img, 'originalUrl');
+ image.alt = img.getAttribute('alt');
+ total += 1;
+ addClass(image, CLASS_FADE);
+ toggleClass(image, CLASS_TRANSITION, options.transition);
+
+ if (hasClass(item, CLASS_ACTIVE)) {
+ addClass(image, CLASS_IN);
+ index = i;
+ }
+
+ list.push(image);
+ addListener(image, EVENT_LOAD, onLoad, {
+ once: true
+ });
+ player.appendChild(image);
+ });
+
+ if (isNumber(options.interval) && options.interval > 0) {
+ var play = function play() {
+ _this3.playing = setTimeout(function () {
+ removeClass(list[index], CLASS_IN);
+ index += 1;
+ index = index < total ? index : 0;
+ addClass(list[index], CLASS_IN);
+ play();
+ }, options.interval);
+ };
+
+ if (total > 1) {
+ play();
+ }
+ }
+
+ return this;
+ },
+
+
+ // Stop play
+ stop: function stop() {
+ var _this4 = this;
+
+ if (!this.played) {
+ return this;
+ }
+
+ var player = this.player;
+
+
+ this.played = false;
+ clearTimeout(this.playing);
+ forEach(player.getElementsByTagName('img'), function (image) {
+ removeListener(image, EVENT_LOAD, _this4.onLoadWhenPlay);
+ });
+ removeClass(player, CLASS_SHOW);
+ player.innerHTML = '';
+ this.exitFullscreen();
+
+ return this;
+ },
+
+
+ // Enter modal mode (only available in inline mode)
+ full: function full() {
+ var _this5 = this;
+
+ var options = this.options,
+ viewer = this.viewer,
+ image = this.image,
+ list = this.list;
+
+
+ if (!this.isShown || this.played || this.fulled || !options.inline) {
+ return this;
+ }
+
+ this.fulled = true;
+ this.open();
+ addClass(this.button, CLASS_FULLSCREEN_EXIT);
+
+ if (options.transition) {
+ removeClass(list, CLASS_TRANSITION);
+
+ if (this.viewed) {
+ removeClass(image, CLASS_TRANSITION);
+ }
+ }
+
+ addClass(viewer, CLASS_FIXED);
+ viewer.setAttribute('style', '');
+ setStyle(viewer, {
+ zIndex: options.zIndex
+ });
+
+ this.initContainer();
+ this.viewerData = assign({}, this.containerData);
+ this.renderList();
+
+ if (this.viewed) {
+ this.initImage(function () {
+ _this5.renderImage(function () {
+ if (options.transition) {
+ setTimeout(function () {
+ addClass(image, CLASS_TRANSITION);
+ addClass(list, CLASS_TRANSITION);
+ }, 0);
+ }
+ });
+ });
+ }
+
+ return this;
+ },
+
+
+ // Exit modal mode (only available in inline mode)
+ exit: function exit() {
+ var _this6 = this;
+
+ var options = this.options,
+ viewer = this.viewer,
+ image = this.image,
+ list = this.list;
+
+
+ if (!this.isShown || this.played || !this.fulled || !options.inline) {
+ return this;
+ }
+
+ this.fulled = false;
+ this.close();
+ removeClass(this.button, CLASS_FULLSCREEN_EXIT);
+
+ if (options.transition) {
+ removeClass(list, CLASS_TRANSITION);
+
+ if (this.viewed) {
+ removeClass(image, CLASS_TRANSITION);
+ }
+ }
+
+ removeClass(viewer, CLASS_FIXED);
+ setStyle(viewer, {
+ zIndex: options.zIndexInline
+ });
+
+ this.viewerData = assign({}, this.parentData);
+ this.renderViewer();
+ this.renderList();
+
+ if (this.viewed) {
+ this.initImage(function () {
+ _this6.renderImage(function () {
+ if (options.transition) {
+ setTimeout(function () {
+ addClass(image, CLASS_TRANSITION);
+ addClass(list, CLASS_TRANSITION);
+ }, 0);
+ }
+ });
+ });
+ }
+
+ return this;
+ },
+
+
+ // Show the current ratio of the image with percentage
+ tooltip: function tooltip() {
+ var _this7 = this;
+
+ var options = this.options,
+ tooltipBox = this.tooltipBox,
+ imageData = this.imageData;
+
+
+ if (!this.viewed || this.played || !options.tooltip) {
+ return this;
+ }
+
+ tooltipBox.textContent = Math.round(imageData.ratio * 100) + '%';
+
+ if (!this.tooltipping) {
+ if (options.transition) {
+ if (this.fading) {
+ dispatchEvent(tooltipBox, EVENT_TRANSITION_END);
+ }
+
+ addClass(tooltipBox, CLASS_SHOW);
+ addClass(tooltipBox, CLASS_FADE);
+ addClass(tooltipBox, CLASS_TRANSITION);
+
+ // Force reflow to enable CSS3 transition
+ // eslint-disable-next-line
+ tooltipBox.offsetWidth;
+ addClass(tooltipBox, CLASS_IN);
+ } else {
+ addClass(tooltipBox, CLASS_SHOW);
+ }
+ } else {
+ clearTimeout(this.tooltipping);
+ }
+
+ this.tooltipping = setTimeout(function () {
+ if (options.transition) {
+ addListener(tooltipBox, EVENT_TRANSITION_END, function () {
+ removeClass(tooltipBox, CLASS_SHOW);
+ removeClass(tooltipBox, CLASS_FADE);
+ removeClass(tooltipBox, CLASS_TRANSITION);
+ _this7.fading = false;
+ }, {
+ once: true
+ });
+
+ removeClass(tooltipBox, CLASS_IN);
+ _this7.fading = true;
+ } else {
+ removeClass(tooltipBox, CLASS_SHOW);
+ }
+
+ _this7.tooltipping = false;
+ }, 1000);
+
+ return this;
+ },
+
+
+ // Toggle the image size between its natural size and initial size
+ toggle: function toggle() {
+ if (this.imageData.ratio === 1) {
+ this.zoomTo(this.initialImageData.ratio, true);
+ } else {
+ this.zoomTo(1, true);
+ }
+
+ return this;
+ },
+
+
+ // Reset the image to its initial state
+ reset: function reset() {
+ if (this.viewed && !this.played) {
+ this.imageData = assign({}, this.initialImageData);
+ this.renderImage();
+ }
+
+ return this;
+ },
+
+
+ // Update viewer when images changed
+ update: function update() {
+ var element = this.element,
+ options = this.options,
+ isImg = this.isImg;
+
+ // Destroy viewer if the target image was deleted
+
+ if (isImg && !element.parentNode) {
+ return this.destroy();
+ }
+
+ var images = [];
+
+ forEach(isImg ? [element] : element.querySelectorAll('img'), function (image) {
+ if (options.filter) {
+ if (options.filter(image)) {
+ images.push(image);
+ }
+ } else {
+ images.push(image);
+ }
+ });
+
+ if (!images.length) {
+ return this;
+ }
+
+ this.images = images;
+ this.length = images.length;
+
+ if (this.ready) {
+ var indexes = [];
+
+ forEach(this.items, function (item, i) {
+ var img = item.querySelector('img');
+ var image = images[i];
+
+ if (image) {
+ if (image.src !== img.src) {
+ indexes.push(i);
+ }
+ } else {
+ indexes.push(i);
+ }
+ });
+
+ setStyle(this.list, {
+ width: 'auto'
+ });
+
+ this.initList();
+
+ if (this.isShown) {
+ if (this.length) {
+ if (this.viewed) {
+ var index = indexes.indexOf(this.index);
+
+ if (index >= 0) {
+ this.viewed = false;
+ this.view(Math.max(this.index - (index + 1), 0));
+ } else {
+ addClass(this.items[this.index], CLASS_ACTIVE);
+ }
+ }
+ } else {
+ this.image = null;
+ this.viewed = false;
+ this.index = 0;
+ this.imageData = {};
+ this.canvas.innerHTML = '';
+ this.title.innerHTML = '';
+ }
+ }
+ } else {
+ this.build();
+ }
+
+ return this;
+ },
+
+
+ // Destroy the viewer
+ destroy: function destroy() {
+ var element = this.element,
+ options = this.options;
+
+
+ if (!getData(element, NAMESPACE)) {
+ return this;
+ }
+
+ this.destroyed = true;
+
+ if (this.ready) {
+ if (this.played) {
+ this.stop();
+ }
+
+ if (options.inline) {
+ if (this.fulled) {
+ this.exit();
+ }
+
+ this.unbind();
+ } else if (this.isShown) {
+ if (this.viewing) {
+ if (this.imageRendering) {
+ this.imageRendering.abort();
+ } else if (this.imageInitializing) {
+ this.imageInitializing.abort();
+ }
+ }
+
+ if (this.hiding) {
+ this.transitioning.abort();
+ }
+
+ this.hidden();
+ } else if (this.showing) {
+ this.transitioning.abort();
+ this.hidden();
+ }
+
+ this.ready = false;
+ this.viewer.parentNode.removeChild(this.viewer);
+ } else if (options.inline) {
+ if (this.delaying) {
+ this.delaying.abort();
+ } else if (this.initializing) {
+ this.initializing.abort();
+ }
+ }
+
+ if (!options.inline) {
+ removeListener(element, EVENT_CLICK, this.onStart);
+ }
+
+ removeData(element, NAMESPACE);
+ return this;
+ }
+ };
+
+ var others = {
+ open: function open() {
+ var body = this.body;
+
+
+ addClass(body, CLASS_OPEN);
+
+ body.style.paddingRight = this.scrollbarWidth + (parseFloat(this.initialBodyPaddingRight) || 0) + 'px';
+ },
+ close: function close() {
+ var body = this.body;
+
+
+ removeClass(body, CLASS_OPEN);
+ body.style.paddingRight = this.initialBodyPaddingRight;
+ },
+ shown: function shown() {
+ var element = this.element,
+ options = this.options;
+
+
+ this.fulled = true;
+ this.isShown = true;
+ this.render();
+ this.bind();
+ this.showing = false;
+
+ if (isFunction(options.shown)) {
+ addListener(element, EVENT_SHOWN, options.shown, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_SHOWN) === false) {
+ return;
+ }
+
+ if (this.ready && this.isShown && !this.hiding) {
+ this.view(this.index);
+ }
+ },
+ hidden: function hidden() {
+ var element = this.element,
+ options = this.options;
+
+
+ this.fulled = false;
+ this.viewed = false;
+ this.isShown = false;
+ this.close();
+ this.unbind();
+ addClass(this.viewer, CLASS_HIDE);
+ this.resetList();
+ this.resetImage();
+ this.hiding = false;
+
+ if (!this.destroyed) {
+ if (isFunction(options.hidden)) {
+ addListener(element, EVENT_HIDDEN, options.hidden, {
+ once: true
+ });
+ }
+
+ dispatchEvent(element, EVENT_HIDDEN);
+ }
+ },
+ requestFullscreen: function requestFullscreen() {
+ var document = this.element.ownerDocument;
+
+ if (this.fulled && !document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
+ var documentElement = document.documentElement;
+
+
+ if (documentElement.requestFullscreen) {
+ documentElement.requestFullscreen();
+ } else if (documentElement.msRequestFullscreen) {
+ documentElement.msRequestFullscreen();
+ } else if (documentElement.mozRequestFullScreen) {
+ documentElement.mozRequestFullScreen();
+ } else if (documentElement.webkitRequestFullscreen) {
+ documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ }
+ }
+ },
+ exitFullscreen: function exitFullscreen() {
+ if (this.fulled) {
+ var document = this.element.ownerDocument;
+
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ }
+ }
+ },
+ change: function change(e) {
+ var options = this.options,
+ pointers = this.pointers;
+
+ var pointer = pointers[Object.keys(pointers)[0]];
+ var offsetX = pointer.endX - pointer.startX;
+ var offsetY = pointer.endY - pointer.startY;
+
+ switch (this.action) {
+ // Move the current image
+ case ACTION_MOVE:
+ this.move(offsetX, offsetY);
+ break;
+
+ // Zoom the current image
+ case ACTION_ZOOM:
+ this.zoom(getMaxZoomRatio(pointers), false, e);
+ break;
+
+ case ACTION_SWITCH:
+ {
+ this.action = 'switched';
+
+ var absoluteOffsetX = Math.abs(offsetX);
+
+ if (absoluteOffsetX > 1 && absoluteOffsetX > Math.abs(offsetY)) {
+ // Empty `pointers` as `touchend` event will not be fired after swiped in iOS browsers.
+ this.pointers = {};
+
+ if (offsetX > 1) {
+ this.prev(options.loop);
+ } else if (offsetX < -1) {
+ this.next(options.loop);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ }
+
+ // Override
+ forEach(pointers, function (p) {
+ p.startX = p.endX;
+ p.startY = p.endY;
+ });
+ },
+ isSwitchable: function isSwitchable() {
+ var imageData = this.imageData,
+ viewerData = this.viewerData;
+
+
+ return this.length > 1 && imageData.left >= 0 && imageData.top >= 0 && imageData.width <= viewerData.width && imageData.height <= viewerData.height;
+ }
+ };
+
+ var AnotherViewer = WINDOW.Viewer;
+
+ var Viewer = function () {
+ /**
+ * Create a new Viewer.
+ * @param {Element} element - The target element for viewing.
+ * @param {Object} [options={}] - The configuration options.
+ */
+ function Viewer(element) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ classCallCheck(this, Viewer);
+
+ if (!element || element.nodeType !== 1) {
+ throw new Error('The first argument is required and must be an element.');
+ }
+
+ this.element = element;
+ this.options = assign({}, DEFAULTS, isPlainObject(options) && options);
+ this.action = false;
+ this.fading = false;
+ this.fulled = false;
+ this.hiding = false;
+ this.imageData = {};
+ this.index = this.options.initialViewIndex;
+ this.isImg = false;
+ this.isShown = false;
+ this.length = 0;
+ this.played = false;
+ this.playing = false;
+ this.pointers = {};
+ this.ready = false;
+ this.showing = false;
+ this.timeout = false;
+ this.tooltipping = false;
+ this.viewed = false;
+ this.viewing = false;
+ this.wheeling = false;
+ this.zooming = false;
+ this.init();
+ }
+
+ createClass(Viewer, [{
+ key: 'init',
+ value: function init() {
+ var _this = this;
+
+ var element = this.element,
+ options = this.options;
+
+
+ if (getData(element, NAMESPACE)) {
+ return;
+ }
+
+ setData(element, NAMESPACE, this);
+
+ var isImg = element.tagName.toLowerCase() === 'img';
+ var images = [];
+
+ forEach(isImg ? [element] : element.querySelectorAll('img'), function (image) {
+ if (isFunction(options.filter)) {
+ if (options.filter.call(_this, image)) {
+ images.push(image);
+ }
+ } else {
+ images.push(image);
+ }
+ });
+
+ if (!images.length) {
+ return;
+ }
+
+ this.isImg = isImg;
+ this.length = images.length;
+ this.images = images;
+
+ var ownerDocument = element.ownerDocument;
+
+ var body = ownerDocument.body || ownerDocument.documentElement;
+
+ this.body = body;
+ this.scrollbarWidth = window.innerWidth - ownerDocument.documentElement.clientWidth;
+ this.initialBodyPaddingRight = window.getComputedStyle(body).paddingRight;
+
+ // Override `transition` option if it is not supported
+ if (isUndefined(document.createElement(NAMESPACE).style.transition)) {
+ options.transition = false;
+ }
+
+ if (options.inline) {
+ var count = 0;
+ var progress = function progress() {
+ count += 1;
+
+ if (count === _this.length) {
+ var timeout = void 0;
+
+ _this.initializing = false;
+ _this.delaying = {
+ abort: function abort() {
+ clearTimeout(timeout);
+ }
+ };
+
+ // build asynchronously to keep `this.viewer` is accessible in `ready` event handler.
+ timeout = setTimeout(function () {
+ _this.delaying = false;
+ _this.build();
+ }, 0);
+ }
+ };
+
+ this.initializing = {
+ abort: function abort() {
+ forEach(images, function (image) {
+ if (!image.complete) {
+ removeListener(image, EVENT_LOAD, progress);
+ }
+ });
+ }
+ };
+
+ forEach(images, function (image) {
+ if (image.complete) {
+ progress();
+ } else {
+ addListener(image, EVENT_LOAD, progress, {
+ once: true
+ });
+ }
+ });
+ } else {
+ addListener(element, EVENT_CLICK, this.onStart = function (_ref) {
+ var target = _ref.target;
+
+ if (target.tagName.toLowerCase() === 'img') {
+ _this.view(_this.images.indexOf(target));
+ }
+ });
+ }
+ }
+ }, {
+ key: 'build',
+ value: function build() {
+ if (this.ready) {
+ return;
+ }
+
+ var element = this.element,
+ options = this.options;
+
+ var parent = element.parentNode;
+ var template = document.createElement('div');
+
+ template.innerHTML = TEMPLATE;
+
+ var viewer = template.querySelector('.' + NAMESPACE + '-container');
+ var title = viewer.querySelector('.' + NAMESPACE + '-title');
+ var toolbar = viewer.querySelector('.' + NAMESPACE + '-toolbar');
+ var navbar = viewer.querySelector('.' + NAMESPACE + '-navbar');
+ var button = viewer.querySelector('.' + NAMESPACE + '-button');
+ var canvas = viewer.querySelector('.' + NAMESPACE + '-canvas');
+
+ this.parent = parent;
+ this.viewer = viewer;
+ this.title = title;
+ this.toolbar = toolbar;
+ this.navbar = navbar;
+ this.button = button;
+ this.canvas = canvas;
+ this.footer = viewer.querySelector('.' + NAMESPACE + '-footer');
+ this.tooltipBox = viewer.querySelector('.' + NAMESPACE + '-tooltip');
+ this.player = viewer.querySelector('.' + NAMESPACE + '-player');
+ this.list = viewer.querySelector('.' + NAMESPACE + '-list');
+
+ addClass(title, !options.title ? CLASS_HIDE : getResponsiveClass(Array.isArray(options.title) ? options.title[0] : options.title));
+ addClass(navbar, !options.navbar ? CLASS_HIDE : getResponsiveClass(options.navbar));
+ toggleClass(button, CLASS_HIDE, !options.button);
+
+ if (options.backdrop) {
+ addClass(viewer, NAMESPACE + '-backdrop');
+
+ if (!options.inline && options.backdrop === true) {
+ setData(canvas, DATA_ACTION, 'hide');
+ }
+ }
+
+ if (options.toolbar) {
+ var list = document.createElement('ul');
+ var custom = isPlainObject(options.toolbar);
+ var zoomButtons = BUTTONS.slice(0, 3);
+ var rotateButtons = BUTTONS.slice(7, 9);
+ var scaleButtons = BUTTONS.slice(9);
+
+ if (!custom) {
+ addClass(toolbar, getResponsiveClass(options.toolbar));
+ }
+
+ forEach(custom ? options.toolbar : BUTTONS, function (value, index) {
+ var deep = custom && isPlainObject(value);
+ var name = custom ? hyphenate(index) : value;
+ var show = deep && !isUndefined(value.show) ? value.show : value;
+
+ if (!show || !options.zoomable && zoomButtons.indexOf(name) !== -1 || !options.rotatable && rotateButtons.indexOf(name) !== -1 || !options.scalable && scaleButtons.indexOf(name) !== -1) {
+ return;
+ }
+
+ var size = deep && !isUndefined(value.size) ? value.size : value;
+ var click = deep && !isUndefined(value.click) ? value.click : value;
+ var item = document.createElement('li');
+
+ item.setAttribute('role', 'button');
+ addClass(item, NAMESPACE + '-' + name);
+
+ if (!isFunction(click)) {
+ setData(item, DATA_ACTION, name);
+ }
+
+ if (isNumber(show)) {
+ addClass(item, getResponsiveClass(show));
+ }
+
+ if (['small', 'large'].indexOf(size) !== -1) {
+ addClass(item, NAMESPACE + '-' + size);
+ } else if (name === 'play') {
+ addClass(item, NAMESPACE + '-large');
+ }
+
+ if (isFunction(click)) {
+ addListener(item, EVENT_CLICK, click);
+ }
+
+ list.appendChild(item);
+ });
+
+ toolbar.appendChild(list);
+ } else {
+ addClass(toolbar, CLASS_HIDE);
+ }
+
+ if (!options.rotatable) {
+ var rotates = toolbar.querySelectorAll('li[class*="rotate"]');
+
+ addClass(rotates, CLASS_INVISIBLE);
+ forEach(rotates, function (rotate) {
+ toolbar.appendChild(rotate);
+ });
+ }
+
+ if (options.inline) {
+ addClass(button, CLASS_FULLSCREEN);
+ setStyle(viewer, {
+ zIndex: options.zIndexInline
+ });
+
+ if (window.getComputedStyle(parent).position === 'static') {
+ setStyle(parent, {
+ position: 'relative'
+ });
+ }
+
+ parent.insertBefore(viewer, element.nextSibling);
+ } else {
+ addClass(button, CLASS_CLOSE);
+ addClass(viewer, CLASS_FIXED);
+ addClass(viewer, CLASS_FADE);
+ addClass(viewer, CLASS_HIDE);
+
+ setStyle(viewer, {
+ zIndex: options.zIndex
+ });
+
+ var container = options.container;
+
+
+ if (isString(container)) {
+ container = element.ownerDocument.querySelector(container);
+ }
+
+ if (!container) {
+ container = this.body;
+ }
+
+ container.appendChild(viewer);
+ }
+
+ if (options.inline) {
+ this.render();
+ this.bind();
+ this.isShown = true;
+ }
+
+ this.ready = true;
+
+ if (isFunction(options.ready)) {
+ addListener(element, EVENT_READY, options.ready, {
+ once: true
+ });
+ }
+
+ if (dispatchEvent(element, EVENT_READY) === false) {
+ this.ready = false;
+ return;
+ }
+
+ if (this.ready && options.inline) {
+ this.view(this.index);
+ }
+ }
+
+ /**
+ * Get the no conflict viewer class.
+ * @returns {Viewer} The viewer class.
+ */
+
+ }], [{
+ key: 'noConflict',
+ value: function noConflict() {
+ window.Viewer = AnotherViewer;
+ return Viewer;
+ }
+
+ /**
+ * Change the default options.
+ * @param {Object} options - The new default options.
+ */
+
+ }, {
+ key: 'setDefaults',
+ value: function setDefaults(options) {
+ assign(DEFAULTS, isPlainObject(options) && options);
+ }
+ }]);
+ return Viewer;
+ }();
+
+ assign(Viewer.prototype, render, events, handlers, methods, others);
+
+ return Viewer;
+
+})));
\ No newline at end of file
diff --git a/public/static/jquery/3.3.1/jquery.min.js b/public/static/jquery/3.3.1/jquery.min.js
new file mode 100644
index 00000000..4d9b3a25
--- /dev/null
+++ b/public/static/jquery/3.3.1/jquery.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML=" ";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML=" ","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML=" ",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/
+JS;
+ return $js;
+ }
+
+ protected function console($type, $msg)
+ {
+ $type = strtolower($type);
+ $trace_tabs = array_values($this->config['tabs']);
+ $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type)
+ ? "console.group('{$type}');"
+ : "console.groupCollapsed('{$type}');";
+
+ foreach ((array) $msg as $key => $m) {
+ switch ($type) {
+ case '调试':
+ $var_type = gettype($m);
+ if (in_array($var_type, ['array', 'string'])) {
+ $line[] = "console.log(" . json_encode($m) . ");";
+ } else {
+ $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");";
+ }
+ break;
+ case '错误':
+ $msg = str_replace("\n", '\n', addslashes(is_scalar($m) ? $m : json_encode($m)));
+ $style = 'color:#F4006B;font-size:14px;';
+ $line[] = "console.error(\"%c{$msg}\", \"{$style}\");";
+ break;
+ case 'sql':
+ $msg = str_replace("\n", '\n', addslashes($m));
+ $style = "color:#009bb4;";
+ $line[] = "console.log(\"%c{$msg}\", \"{$style}\");";
+ break;
+ default:
+ $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m;
+ $msg = json_encode($m);
+ $line[] = "console.log({$msg});";
+ break;
+ }
+ }
+ $line[] = "console.groupEnd();";
+ return implode(PHP_EOL, $line);
+ }
+
+}
diff --git a/thinkphp/library/think/debug/Html.php b/thinkphp/library/think/debug/Html.php
new file mode 100644
index 00000000..85f354af
--- /dev/null
+++ b/thinkphp/library/think/debug/Html.php
@@ -0,0 +1,106 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\debug;
+
+use think\Container;
+use think\Db;
+use think\Response;
+
+/**
+ * 页面Trace调试
+ */
+class Html
+{
+ protected $config = [
+ 'file' => '',
+ 'tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'],
+ ];
+
+ // 实例化并传入参数
+ public function __construct(array $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 调试输出接口
+ * @access public
+ * @param Response $response Response对象
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function output(Response $response, array $log = [])
+ {
+ $request = Container::get('request');
+ $contentType = $response->getHeader('Content-Type');
+ $accept = $request->header('accept');
+ if (strpos($accept, 'application/json') === 0 || $request->isAjax()) {
+ return false;
+ } elseif (!empty($contentType) && strpos($contentType, 'html') === false) {
+ return false;
+ }
+ // 获取基本信息
+ $runtime = number_format(microtime(true) - Container::get('app')->getBeginTime(), 10, '.', '');
+ $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';
+ $mem = number_format((memory_get_usage() - Container::get('app')->getBeginMem()) / 1024, 2);
+
+ // 页面Trace信息
+ if (isset($_SERVER['HTTP_HOST'])) {
+ $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ } else {
+ $uri = 'cmd:' . implode(' ', $_SERVER['argv']);
+ }
+ $base = [
+ '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri,
+ '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()),
+ '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ',
+ '缓存信息' => Container::get('cache')->getReadTimes() . ' reads,' . Container::get('cache')->getWriteTimes() . ' writes',
+ ];
+
+ if (session_id()) {
+ $base['会话信息'] = 'SESSION_ID=' . session_id();
+ }
+
+ $info = Container::get('debug')->getFile(true);
+
+ // 页面Trace信息
+ $trace = [];
+ foreach ($this->config['tabs'] as $name => $title) {
+ $name = strtolower($name);
+ switch ($name) {
+ case 'base': // 基本信息
+ $trace[$title] = $base;
+ break;
+ case 'file': // 文件信息
+ $trace[$title] = $info;
+ break;
+ default: // 调试信息
+ if (strpos($name, '|')) {
+ // 多组信息
+ $names = explode('|', $name);
+ $result = [];
+ foreach ($names as $name) {
+ $result = array_merge($result, isset($log[$name]) ? $log[$name] : []);
+ }
+ $trace[$title] = $result;
+ } else {
+ $trace[$title] = isset($log[$name]) ? $log[$name] : '';
+ }
+ }
+ }
+ // 调用Trace页面模板
+ ob_start();
+ include $this->config['file'];
+ return ob_get_clean();
+ }
+
+}
diff --git a/thinkphp/library/think/exception/ClassNotFoundException.php b/thinkphp/library/think/exception/ClassNotFoundException.php
new file mode 100644
index 00000000..eb22e730
--- /dev/null
+++ b/thinkphp/library/think/exception/ClassNotFoundException.php
@@ -0,0 +1,32 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class ClassNotFoundException extends \RuntimeException
+{
+ protected $class;
+ public function __construct($message, $class = '')
+ {
+ $this->message = $message;
+ $this->class = $class;
+ }
+
+ /**
+ * 获取类名
+ * @access public
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+}
diff --git a/thinkphp/library/think/exception/DbException.php b/thinkphp/library/think/exception/DbException.php
new file mode 100644
index 00000000..0f504257
--- /dev/null
+++ b/thinkphp/library/think/exception/DbException.php
@@ -0,0 +1,44 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+use think\Exception;
+
+/**
+ * Database相关异常处理类
+ */
+class DbException extends Exception
+{
+ /**
+ * DbException constructor.
+ * @access public
+ * @param string $message
+ * @param array $config
+ * @param string $sql
+ * @param int $code
+ */
+ public function __construct($message, array $config, $sql, $code = 10500)
+ {
+ $this->message = $message;
+ $this->code = $code;
+
+ $this->setData('Database Status', [
+ 'Error Code' => $code,
+ 'Error Message' => $message,
+ 'Error SQL' => $sql,
+ ]);
+
+ unset($config['username'], $config['password']);
+ $this->setData('Database Config', $config);
+ }
+
+}
diff --git a/thinkphp/library/think/exception/ErrorException.php b/thinkphp/library/think/exception/ErrorException.php
new file mode 100644
index 00000000..3143b8f7
--- /dev/null
+++ b/thinkphp/library/think/exception/ErrorException.php
@@ -0,0 +1,56 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+use think\Exception;
+
+/**
+ * ThinkPHP错误异常
+ * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误
+ * 除开从 think\Exception 继承的功能
+ * 其他和PHP系统\ErrorException功能基本一样
+ */
+class ErrorException extends Exception
+{
+ /**
+ * 用于保存错误级别
+ * @var integer
+ */
+ protected $severity;
+
+ /**
+ * 错误异常构造函数
+ * @access public
+ * @param integer $severity 错误级别
+ * @param string $message 错误详细信息
+ * @param string $file 出错文件路径
+ * @param integer $line 出错行号
+ */
+ public function __construct($severity, $message, $file, $line)
+ {
+ $this->severity = $severity;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ $this->code = 0;
+ }
+
+ /**
+ * 获取错误级别
+ * @access public
+ * @return integer 错误级别
+ */
+ final public function getSeverity()
+ {
+ return $this->severity;
+ }
+}
diff --git a/thinkphp/library/think/exception/Handle.php b/thinkphp/library/think/exception/Handle.php
new file mode 100644
index 00000000..02c85ec1
--- /dev/null
+++ b/thinkphp/library/think/exception/Handle.php
@@ -0,0 +1,306 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+use Exception;
+use think\console\Output;
+use think\Container;
+use think\Response;
+
+class Handle
+{
+ protected $render;
+ protected $ignoreReport = [
+ '\\think\\exception\\HttpException',
+ ];
+
+ public function setRender($render)
+ {
+ $this->render = $render;
+ }
+
+ /**
+ * Report or log an exception.
+ *
+ * @access public
+ * @param \Exception $exception
+ * @return void
+ */
+ public function report(Exception $exception)
+ {
+ if (!$this->isIgnoreReport($exception)) {
+ // 收集异常数据
+ if (Container::get('app')->isDebug()) {
+ $data = [
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'message' => $this->getMessage($exception),
+ 'code' => $this->getCode($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]";
+ } else {
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ ];
+ $log = "[{$data['code']}]{$data['message']}";
+ }
+
+ if (Container::get('app')->config('log.record_trace')) {
+ $log .= "\r\n" . $exception->getTraceAsString();
+ }
+
+ Container::get('log')->record($log, 'error');
+ }
+ }
+
+ protected function isIgnoreReport(Exception $exception)
+ {
+ foreach ($this->ignoreReport as $class) {
+ if ($exception instanceof $class) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Render an exception into an HTTP response.
+ *
+ * @access public
+ * @param \Exception $e
+ * @return Response
+ */
+ public function render(Exception $e)
+ {
+ if ($this->render && $this->render instanceof \Closure) {
+ $result = call_user_func_array($this->render, [$e]);
+
+ if ($result) {
+ return $result;
+ }
+ }
+
+ if ($e instanceof HttpException) {
+ return $this->renderHttpException($e);
+ } else {
+ return $this->convertExceptionToResponse($e);
+ }
+ }
+
+ /**
+ * @access public
+ * @param Output $output
+ * @param Exception $e
+ */
+ public function renderForConsole(Output $output, Exception $e)
+ {
+ if (Container::get('app')->isDebug()) {
+ $output->setVerbosity(Output::VERBOSITY_DEBUG);
+ }
+
+ $output->renderException($e);
+ }
+
+ /**
+ * @access protected
+ * @param HttpException $e
+ * @return Response
+ */
+ protected function renderHttpException(HttpException $e)
+ {
+ $status = $e->getStatusCode();
+ $template = Container::get('app')->config('http_exception_template');
+
+ if (!Container::get('app')->isDebug() && !empty($template[$status])) {
+ return Response::create($template[$status], 'view', $status)->assign(['e' => $e]);
+ } else {
+ return $this->convertExceptionToResponse($e);
+ }
+ }
+
+ /**
+ * @access protected
+ * @param Exception $exception
+ * @return Response
+ */
+ protected function convertExceptionToResponse(Exception $exception)
+ {
+ // 收集异常数据
+ if (Container::get('app')->isDebug()) {
+ // 调试模式,获取详细的错误信息
+ $data = [
+ 'name' => get_class($exception),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'message' => $this->getMessage($exception),
+ 'trace' => $exception->getTrace(),
+ 'code' => $this->getCode($exception),
+ 'source' => $this->getSourceCode($exception),
+ 'datas' => $this->getExtendData($exception),
+ 'tables' => [
+ 'GET Data' => $_GET,
+ 'POST Data' => $_POST,
+ 'Files' => $_FILES,
+ 'Cookies' => $_COOKIE,
+ 'Session' => isset($_SESSION) ? $_SESSION : [],
+ 'Server/Request Data' => $_SERVER,
+ 'Environment Variables' => $_ENV,
+ 'ThinkPHP Constants' => $this->getConst(),
+ ],
+ ];
+ } else {
+ // 部署模式仅显示 Code 和 Message
+ $data = [
+ 'code' => $this->getCode($exception),
+ 'message' => $this->getMessage($exception),
+ ];
+
+ if (!Container::get('app')->config('show_error_msg')) {
+ // 不显示详细错误信息
+ $data['message'] = Container::get('app')->config('error_message');
+ }
+ }
+
+ //保留一层
+ while (ob_get_level() > 1) {
+ ob_end_clean();
+ }
+
+ $data['echo'] = ob_get_clean();
+
+ ob_start();
+ extract($data);
+ include Container::get('app')->config('exception_tmpl');
+
+ // 获取并清空缓存
+ $content = ob_get_clean();
+ $response = Response::create($content, 'html');
+
+ if ($exception instanceof HttpException) {
+ $statusCode = $exception->getStatusCode();
+ $response->header($exception->getHeaders());
+ }
+
+ if (!isset($statusCode)) {
+ $statusCode = 500;
+ }
+ $response->code($statusCode);
+
+ return $response;
+ }
+
+ /**
+ * 获取错误编码
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param \Exception $exception
+ * @return integer 错误编码
+ */
+ protected function getCode(Exception $exception)
+ {
+ $code = $exception->getCode();
+
+ if (!$code && $exception instanceof ErrorException) {
+ $code = $exception->getSeverity();
+ }
+
+ return $code;
+ }
+
+ /**
+ * 获取错误信息
+ * ErrorException则使用错误级别作为错误编码
+ * @access protected
+ * @param \Exception $exception
+ * @return string 错误信息
+ */
+ protected function getMessage(Exception $exception)
+ {
+ $message = $exception->getMessage();
+
+ if (PHP_SAPI == 'cli') {
+ return $message;
+ }
+
+ $lang = Container::get('lang');
+
+ if (strpos($message, ':')) {
+ $name = strstr($message, ':', true);
+ $message = $lang->has($name) ? $lang->get($name) . strstr($message, ':') : $message;
+ } elseif (strpos($message, ',')) {
+ $name = strstr($message, ',', true);
+ $message = $lang->has($name) ? $lang->get($name) . ':' . substr(strstr($message, ','), 1) : $message;
+ } elseif ($lang->has($message)) {
+ $message = $lang->get($message);
+ }
+
+ return $message;
+ }
+
+ /**
+ * 获取出错文件内容
+ * 获取错误的前9行和后9行
+ * @access protected
+ * @param \Exception $exception
+ * @return array 错误文件内容
+ */
+ protected function getSourceCode(Exception $exception)
+ {
+ // 读取前9行和后9行
+ $line = $exception->getLine();
+ $first = ($line - 9 > 0) ? $line - 9 : 1;
+
+ try {
+ $contents = file($exception->getFile());
+ $source = [
+ 'first' => $first,
+ 'source' => array_slice($contents, $first - 1, 19),
+ ];
+ } catch (Exception $e) {
+ $source = [];
+ }
+
+ return $source;
+ }
+
+ /**
+ * 获取异常扩展信息
+ * 用于非调试模式html返回类型显示
+ * @access protected
+ * @param \Exception $exception
+ * @return array 异常类定义的扩展数据
+ */
+ protected function getExtendData(Exception $exception)
+ {
+ $data = [];
+
+ if ($exception instanceof \think\Exception) {
+ $data = $exception->getData();
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取常量列表
+ * @access private
+ * @return array 常量列表
+ */
+ private static function getConst()
+ {
+ $const = get_defined_constants(true);
+
+ return isset($const['user']) ? $const['user'] : [];
+ }
+}
diff --git a/thinkphp/library/think/exception/HttpException.php b/thinkphp/library/think/exception/HttpException.php
new file mode 100644
index 00000000..01a27fc2
--- /dev/null
+++ b/thinkphp/library/think/exception/HttpException.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class HttpException extends \RuntimeException
+{
+ private $statusCode;
+ private $headers;
+
+ public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0)
+ {
+ $this->statusCode = $statusCode;
+ $this->headers = $headers;
+
+ parent::__construct($message, $code, $previous);
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+}
diff --git a/thinkphp/library/think/exception/HttpResponseException.php b/thinkphp/library/think/exception/HttpResponseException.php
new file mode 100644
index 00000000..52972867
--- /dev/null
+++ b/thinkphp/library/think/exception/HttpResponseException.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+use think\Response;
+
+class HttpResponseException extends \RuntimeException
+{
+ /**
+ * @var Response
+ */
+ protected $response;
+
+ public function __construct(Response $response)
+ {
+ $this->response = $response;
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+}
diff --git a/thinkphp/library/think/exception/PDOException.php b/thinkphp/library/think/exception/PDOException.php
new file mode 100644
index 00000000..25240b68
--- /dev/null
+++ b/thinkphp/library/think/exception/PDOException.php
@@ -0,0 +1,40 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+/**
+ * PDO异常处理类
+ * 重新封装了系统的\PDOException类
+ */
+class PDOException extends DbException
+{
+ /**
+ * PDOException constructor.
+ * @access public
+ * @param \PDOException $exception
+ * @param array $config
+ * @param string $sql
+ * @param int $code
+ */
+ public function __construct(\PDOException $exception, array $config, $sql, $code = 10501)
+ {
+ $error = $exception->errorInfo;
+
+ $this->setData('PDO Error Info', [
+ 'SQLSTATE' => $error[0],
+ 'Driver Error Code' => isset($error[1]) ? $error[1] : 0,
+ 'Driver Error Message' => isset($error[2]) ? $error[2] : '',
+ ]);
+
+ parent::__construct($exception->getMessage(), $config, $sql, $code);
+ }
+}
diff --git a/thinkphp/library/think/exception/RouteNotFoundException.php b/thinkphp/library/think/exception/RouteNotFoundException.php
new file mode 100644
index 00000000..d22e3a63
--- /dev/null
+++ b/thinkphp/library/think/exception/RouteNotFoundException.php
@@ -0,0 +1,22 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class RouteNotFoundException extends HttpException
+{
+
+ public function __construct()
+ {
+ parent::__construct(404, 'Route Not Found');
+ }
+
+}
diff --git a/thinkphp/library/think/exception/TemplateNotFoundException.php b/thinkphp/library/think/exception/TemplateNotFoundException.php
new file mode 100644
index 00000000..42020693
--- /dev/null
+++ b/thinkphp/library/think/exception/TemplateNotFoundException.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class TemplateNotFoundException extends \RuntimeException
+{
+ protected $template;
+
+ public function __construct($message, $template = '')
+ {
+ $this->message = $message;
+ $this->template = $template;
+ }
+
+ /**
+ * 获取模板文件
+ * @access public
+ * @return string
+ */
+ public function getTemplate()
+ {
+ return $this->template;
+ }
+}
diff --git a/thinkphp/library/think/exception/ThrowableError.php b/thinkphp/library/think/exception/ThrowableError.php
new file mode 100644
index 00000000..87b6b9d7
--- /dev/null
+++ b/thinkphp/library/think/exception/ThrowableError.php
@@ -0,0 +1,47 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class ThrowableError extends \ErrorException
+{
+ public function __construct(\Throwable $e)
+ {
+
+ if ($e instanceof \ParseError) {
+ $message = 'Parse error: ' . $e->getMessage();
+ $severity = E_PARSE;
+ } elseif ($e instanceof \TypeError) {
+ $message = 'Type error: ' . $e->getMessage();
+ $severity = E_RECOVERABLE_ERROR;
+ } else {
+ $message = 'Fatal error: ' . $e->getMessage();
+ $severity = E_ERROR;
+ }
+
+ parent::__construct(
+ $message,
+ $e->getCode(),
+ $severity,
+ $e->getFile(),
+ $e->getLine()
+ );
+
+ $this->setTrace($e->getTrace());
+ }
+
+ protected function setTrace($trace)
+ {
+ $traceReflector = new \ReflectionProperty('Exception', 'trace');
+ $traceReflector->setAccessible(true);
+ $traceReflector->setValue($this, $trace);
+ }
+}
diff --git a/thinkphp/library/think/exception/ValidateException.php b/thinkphp/library/think/exception/ValidateException.php
new file mode 100644
index 00000000..b3684169
--- /dev/null
+++ b/thinkphp/library/think/exception/ValidateException.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\exception;
+
+class ValidateException extends \RuntimeException
+{
+ protected $error;
+
+ public function __construct($error)
+ {
+ $this->error = $error;
+ $this->message = is_array($error) ? implode("\n\r", $error) : $error;
+ }
+
+ /**
+ * 获取验证错误信息
+ * @access public
+ * @return array|string
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+}
diff --git a/thinkphp/library/think/facade/App.php b/thinkphp/library/think/facade/App.php
new file mode 100644
index 00000000..37d99510
--- /dev/null
+++ b/thinkphp/library/think/facade/App.php
@@ -0,0 +1,54 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\App
+ * @mixin \think\App
+ * @method \think\App bind(string $bind) static 绑定模块或者控制器
+ * @method void initialize() static 初始化应用
+ * @method void init(string $module='') static 初始化模块
+ * @method \think\Response run() static 执行应用
+ * @method \think\App dispatch(\think\route\Dispatch $dispatch) static 设置当前请求的调度信息
+ * @method void log(mixed $log, string $type = 'info') static 记录调试信息
+ * @method mixed config(string $name='') static 获取配置参数
+ * @method \think\route\Dispatch routeCheck() static URL路由检测(根据PATH_INFO)
+ * @method \think\App routeMust(bool $must = false) static 设置应用的路由检测机制
+ * @method \think\Model model(string $name = '', string $layer = 'model', bool $appendSuffix = false, string $common = 'common') static 实例化模型
+ * @method object controller(string $name, string $layer = 'controller', bool $appendSuffix = false, string $empty = '') static 实例化控制器
+ * @method \think\Validate validate(string $name = '', string $layer = 'validate', bool $appendSuffix = false, string $common = 'common') static 实例化验证器类
+ * @method \think\db\Query db(mixed $config = [], mixed $name = false) static 数据库初始化
+ * @method mixed action(string $url, $vars = [], $layer = 'controller', $appendSuffix = false) static 调用模块的操作方法
+ * @method string parseClass(string $module, string $layer, string $name, bool $appendSuffix = false) static 解析应用类的类名
+ * @method string version() static 获取框架版本
+ * @method bool isDebug() static 是否为调试模式
+ * @method string getModulePath() static 获取当前模块路径
+ * @method void setModulePath(string $path) static 设置当前模块路径
+ * @method string getRootPath() static 获取应用根目录
+ * @method string getAppPath() static 获取应用类库目录
+ * @method string getRuntimePath() static 获取应用运行时目录
+ * @method string getThinkPath() static 获取核心框架目录
+ * @method string getRoutePath() static 获取路由目录
+ * @method string getConfigPath() static 获取应用配置目录
+ * @method string getConfigExt() static 获取配置后缀
+ * @method string setNamespace(string $namespace) static 设置应用类库命名空间
+ * @method string getNamespace() static 获取应用类库命名空间
+ * @method string getSuffix() static 是否启用类库后缀
+ * @method float getBeginTime() static 获取应用开启时间
+ * @method integer getBeginMem() static 获取应用初始内存占用
+ * @method \think\Container container() static 获取容器实例
+ */
+class App extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Build.php b/thinkphp/library/think/facade/Build.php
new file mode 100644
index 00000000..46596236
--- /dev/null
+++ b/thinkphp/library/think/facade/Build.php
@@ -0,0 +1,24 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Build
+ * @mixin \think\Build
+ * @method void run(array $build = [], string $namespace = 'app', bool $suffix = false) static 根据传入的build资料创建目录和文件
+ * @method void module(string $module = '', array $list = [], string $namespace = 'app', bool $suffix = false) static 创建模块
+ */
+class Build extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Cache.php b/thinkphp/library/think/facade/Cache.php
new file mode 100644
index 00000000..3011d6b3
--- /dev/null
+++ b/thinkphp/library/think/facade/Cache.php
@@ -0,0 +1,36 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Cache
+ * @mixin \think\Cache
+ * @method \think\cache\Driver connect(array $options = [], mixed $name = false) static 连接缓存
+ * @method \think\cache\Driver init(array $options = []) static 初始化缓存
+ * @method \think\cache\Driver store(string $name = '') static 切换缓存类型
+ * @method bool has(string $name) static 判断缓存是否存在
+ * @method mixed get(string $name, mixed $default = false) static 读取缓存
+ * @method mixed pull(string $name) static 读取缓存并删除
+ * @method mixed set(string $name, mixed $value, int $expire = null) static 设置缓存
+ * @method mixed remember(string $name, mixed $value, int $expire = null) static 如果不存在则写入缓存
+ * @method mixed inc(string $name, int $step = 1) static 自增缓存(针对数值缓存)
+ * @method mixed dec(string $name, int $step = 1) static 自减缓存(针对数值缓存)
+ * @method bool rm(string $name) static 删除缓存
+ * @method bool clear(string $tag = null) static 清除缓存
+ * @method mixed tag(string $name, mixed $keys = null, bool $overlay = false) static 缓存标签
+ * @method object handler() static 返回句柄对象,可执行其它高级方法
+ */
+class Cache extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Config.php b/thinkphp/library/think/facade/Config.php
new file mode 100644
index 00000000..3332413b
--- /dev/null
+++ b/thinkphp/library/think/facade/Config.php
@@ -0,0 +1,27 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Config
+ * @mixin \think\Config
+ * @method bool has(string $name) static 检测配置是否存在
+ * @method array pull(string $name) static 获取一级配置
+ * @method mixed get(string $name) static 获取配置参数
+ * @method mixed set(string $name, mixed $value = null) static 设置配置参数
+ * @method array reset(string $prefix ='') static 重置配置参数
+ */
+class Config extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Cookie.php b/thinkphp/library/think/facade/Cookie.php
new file mode 100644
index 00000000..18efbde0
--- /dev/null
+++ b/thinkphp/library/think/facade/Cookie.php
@@ -0,0 +1,30 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Cookie
+ * @mixin \think\Cookie
+ * @method void init(array $config = []) static 初始化
+ * @method bool has(string $name,string $prefix = null) static 判断Cookie数据
+ * @method mixed prefix(string $prefix = '') static 设置或者获取cookie作用域(前缀)
+ * @method mixed get(string $name,string $prefix = null) static Cookie获取
+ * @method mixed set(string $name, mixed $value = null, mixed $option = null) static 设置Cookie
+ * @method void forever(string $name, mixed $value = null, mixed $option = null) static 永久保存Cookie数据
+ * @method void delete(string $name, string $prefix = null) static Cookie删除
+ * @method void clear($prefix = null) static Cookie清空
+ */
+class Cookie extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Debug.php b/thinkphp/library/think/facade/Debug.php
new file mode 100644
index 00000000..ac482a41
--- /dev/null
+++ b/thinkphp/library/think/facade/Debug.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Debug
+ * @mixin \think\Debug
+ * @method void remark(string $name, mixed $value = '') static 记录时间(微秒)和内存使用情况
+ * @method int getRangeTime(string $start, string $end, mixed $dec = 6) static 统计某个区间的时间(微秒)使用情况
+ * @method int getUseTime(int $dec = 6) static 统计从开始到统计时的时间(微秒)使用情况
+ * @method string getThroughputRate(string $start, string $end, mixed $dec = 6) static 获取当前访问的吞吐率情况
+ * @method string getRangeMem(string $start, string $end, mixed $dec = 2) static 记录区间的内存使用情况
+ * @method int getUseMem(int $dec = 2) static 统计从开始到统计时的内存使用情况
+ * @method string getMemPeak(string $start, string $end, mixed $dec = 2) static 统计区间的内存峰值情况
+ * @method mixed getFile(bool $detail = false) static 获取文件加载信息
+ * @method mixed dump(mixed $var, bool $echo = true, string $label = null, int $flags = ENT_SUBSTITUTE) static 浏览器友好的变量输出
+ */
+class Debug extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Env.php b/thinkphp/library/think/facade/Env.php
new file mode 100644
index 00000000..d430d683
--- /dev/null
+++ b/thinkphp/library/think/facade/Env.php
@@ -0,0 +1,25 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Env
+ * @mixin \think\Env
+ * @method void load(string $file) static 读取环境变量定义文件
+ * @method mixed get(string $name = null, mixed $default = null) static 获取环境变量值
+ * @method void set(mixed $env, string $value = null) static 设置环境变量值
+ */
+class Env extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Hook.php b/thinkphp/library/think/facade/Hook.php
new file mode 100644
index 00000000..e18f83b5
--- /dev/null
+++ b/thinkphp/library/think/facade/Hook.php
@@ -0,0 +1,28 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Hook
+ * @mixin \think\Hook
+ * @method \think\Hook alias(mixed $name, mixed $behavior = null) static 指定行为标识
+ * @method void add(string $tag, mixed $behavior, bool $first = false) static 动态添加行为扩展到某个标签
+ * @method void import(array $tags, bool $recursive = true) static 批量导入插件
+ * @method array get(string $tag = '') static 获取插件信息
+ * @method mixed listen(string $tag, mixed $params = null, bool $once = false) static 监听标签的行为
+ * @method mixed exec(mixed $class, mixed $params = null) static 执行行为
+ */
+class Hook extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Lang.php b/thinkphp/library/think/facade/Lang.php
new file mode 100644
index 00000000..3eb5d4ac
--- /dev/null
+++ b/thinkphp/library/think/facade/Lang.php
@@ -0,0 +1,32 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Lang
+ * @mixin \think\Lang
+ * @method mixed range($range = '') static 设定当前的语言
+ * @method mixed set(mixed $name, string $value = null, string $range = '') static 设置语言定义
+ * @method array load(mixed $file, string $range = '') static 加载语言定义
+ * @method mixed get(string $name = null, array $vars = [], string $range = '') static 获取语言定义
+ * @method mixed has(string $name, string $range = '') static 获取语言定义
+ * @method string detect() static 自动侦测设置获取语言选择
+ * @method void saveToCookie(string $lang = null) static 设置当前语言到Cookie
+ * @method void setLangDetectVar(string $var) static 设置语言自动侦测的变量
+ * @method void setLangCookieVar(string $var) static 设置语言的cookie保存变量
+ * @method void setAllowLangList(array $list) static 设置允许的语言列表
+ */
+class Lang extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Log.php b/thinkphp/library/think/facade/Log.php
new file mode 100644
index 00000000..3a1fbf88
--- /dev/null
+++ b/thinkphp/library/think/facade/Log.php
@@ -0,0 +1,40 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Log
+ * @mixin \think\Log
+ * @method \think\Log init(array $config = []) static 日志初始化
+ * @method mixed getLog(string $type = '') static 获取日志信息
+ * @method \think\Log record(mixed $msg, string $type = 'info', array $context = []) static 记录日志信息
+ * @method \think\Log clear() static 清空日志信息
+ * @method \think\Log key(string $key) static 当前日志记录的授权key
+ * @method bool check(array $config) static 检查日志写入权限
+ * @method bool save() static 保存调试信息
+ * @method void write(mixed $msg, string $type = 'info', bool $force = false) static 实时写入日志信息
+ * @method void log(string $level,mixed $message, array $context = []) static 记录日志信息
+ * @method void emergency(mixed $message, array $context = []) static 记录emergency信息
+ * @method void alert(mixed $message, array $context = []) static 记录alert信息
+ * @method void critical(mixed $message, array $context = []) static 记录critical信息
+ * @method void error(mixed $message, array $context = []) static 记录error信息
+ * @method void warning(mixed $message, array $context = []) static 记录warning信息
+ * @method void notice(mixed $message, array $context = []) static 记录notice信息
+ * @method void info(mixed $message, array $context = []) static 记录info信息
+ * @method void debug(mixed $message, array $context = []) static 记录debug信息
+ * @method void sql(mixed $message, array $context = []) static 记录sql信息
+ */
+class Log extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Middleware.php b/thinkphp/library/think/facade/Middleware.php
new file mode 100644
index 00000000..46827964
--- /dev/null
+++ b/thinkphp/library/think/facade/Middleware.php
@@ -0,0 +1,27 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Middleware
+ * @mixin \think\Middleware
+ * @method void import(array $middlewares = []) static 批量设置中间件
+ * @method void add(mixed $middleware) static 添加中间件到队列
+ * @method void unshift(mixed $middleware) static 添加中间件到队列开头
+ * @method array all() static 获取中间件队列
+ * @method \think\Response dispatch(\think\Request $request) static 执行中间件调度
+ */
+class Middleware extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Request.php b/thinkphp/library/think/facade/Request.php
new file mode 100644
index 00000000..d0eedb24
--- /dev/null
+++ b/thinkphp/library/think/facade/Request.php
@@ -0,0 +1,88 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Request
+ * @mixin \think\Request
+ * @method void hook(mixed $method, mixed $callback = null) static Hook 方法注入
+ * @method \think\Request create(string $uri, string $method = 'GET', array $params = [], array $cookie = [], array $files = [], array $server = [], string $content = null) static 创建一个URL请求
+ * @method mixed domain(bool $port = false) static 获取当前包含协议、端口的域名
+ * @method mixed url(bool $domain = false) static 获取当前完整URL
+ * @method mixed baseUrl(bool $domain = false) static 获取当前URL
+ * @method mixed baseFile(bool $domain = false) static 获取当前执行的文件
+ * @method mixed root(bool $domain = false) static 获取URL访问根地址
+ * @method string rootUrl() static 获取URL访问根目录
+ * @method string pathinfo() static 获取当前请求URL的pathinfo信息(含URL后缀)
+ * @method string path() static 获取当前请求URL的pathinfo信息(不含URL后缀)
+ * @method string ext() static 当前URL的访问后缀
+ * @method float time(bool $float = false) static 获取当前请求的时间
+ * @method mixed type() static 当前请求的资源类型
+ * @method void mimeType(mixed $type, string $val = '') static 设置资源类型
+ * @method string method(bool $method = false) static 当前的请求类型
+ * @method bool isGet() static 是否为GET请求
+ * @method bool isPost() static 是否为POST请求
+ * @method bool isPut() static 是否为PUT请求
+ * @method bool isDelete() static 是否为DELTE请求
+ * @method bool isHead() static 是否为HEAD请求
+ * @method bool isPatch() static 是否为PATCH请求
+ * @method bool isOptions() static 是否为OPTIONS请求
+ * @method bool isCli() static 是否为cli
+ * @method bool isCgi() static 是否为cgi
+ * @method mixed param(string $name = '', mixed $default = null, mixed $filter = '') static 获取当前请求的参数
+ * @method mixed route(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取路由参数
+ * @method mixed get(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取GET参数
+ * @method mixed post(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取POST参数
+ * @method mixed put(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PUT参数
+ * @method mixed delete(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取DELETE参数
+ * @method mixed patch(string $name = '', mixed $default = null, mixed $filter = '') static 设置获取PATCH参数
+ * @method mixed request(string $name = '', mixed $default = null, mixed $filter = '') static 获取request变量
+ * @method mixed session(string $name = '', mixed $default = null, mixed $filter = '') static 获取session数据
+ * @method mixed cookie(string $name = '', mixed $default = null, mixed $filter = '') static 获取cookie参数
+ * @method mixed server(string $name = '', mixed $default = null, mixed $filter = '') static 获取server参数
+ * @method mixed env(string $name = '', mixed $default = null, mixed $filter = '') static 获取环境变量
+ * @method mixed file(string $name = '') static 获取上传的文件信息
+ * @method mixed header(string $name = '', mixed $default = null) static 设置或者获取当前的Header
+ * @method mixed input(array $data,mixed $name = '', mixed $default = null, mixed $filter = '') static 获取变量 支持过滤和默认值
+ * @method mixed filter(mixed $filter = null) static 设置或获取当前的过滤规则
+ * @method mixed has(string $name, string $type = 'param', bool $checkEmpty = false) static 是否存在某个请求参数
+ * @method mixed only(mixed $name, string $type = 'param') static 获取指定的参数
+ * @method mixed except(mixed $name, string $type = 'param') static 排除指定参数获取
+ * @method bool isSsl() static 当前是否ssl
+ * @method bool isAjax(bool $ajax = false) static 当前是否Ajax请求
+ * @method bool isPjax(bool $pjax = false) static 当前是否Pjax请求
+ * @method mixed ip(int $type = 0, bool $adv = true) static 获取客户端IP地址
+ * @method bool isMobile() static 检测是否使用手机访问
+ * @method string scheme() static 当前URL地址中的scheme参数
+ * @method string query() static 当前请求URL地址中的query参数
+ * @method string host(bool $stric = false) static 当前请求的host
+ * @method string port() static 当前请求URL地址中的port参数
+ * @method string protocol() static 当前请求 SERVER_PROTOCOL
+ * @method string remotePort() static 当前请求 REMOTE_PORT
+ * @method string contentType() static 当前请求 HTTP_CONTENT_TYPE
+ * @method array routeInfo() static 获取当前请求的路由信息
+ * @method array dispatch() static 获取当前请求的调度信息
+ * @method string module() static 获取当前的模块名
+ * @method string controller(bool $convert = false) static 获取当前的控制器名
+ * @method string action(bool $convert = false) static 获取当前的操作名
+ * @method string langset() static 获取当前的语言
+ * @method string getContent() static 设置或者获取当前请求的content
+ * @method string getInput() static 获取当前请求的php://input
+ * @method string token(string $name = '__token__', mixed $type = 'md5') static 生成请求令牌
+ * @method string cache(string $key, mixed $expire = null, array $except = [], string $tag = null) static 设置当前地址的请求缓存
+ * @method string getCache() static 读取请求缓存设置
+ */
+class Request extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Response.php b/thinkphp/library/think/facade/Response.php
new file mode 100644
index 00000000..4a4de712
--- /dev/null
+++ b/thinkphp/library/think/facade/Response.php
@@ -0,0 +1,38 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Response
+ * @mixin \think\Response
+ * @method \think\response create(mixed $data = '', string $type = '', int $code = 200, array $header = [], array $options = []) static 创建Response对象
+ * @method void send() static 发送数据到客户端
+ * @method \think\Response options(mixed $options = []) static 输出的参数
+ * @method \think\Response data(mixed $data) static 输出数据设置
+ * @method \think\Response header(mixed $name, string $value = null) static 设置响应头
+ * @method \think\Response content(mixed $content) static 设置页面输出内容
+ * @method \think\Response code(int $code) static 发送HTTP状态
+ * @method \think\Response lastModified(string $time) static LastModified
+ * @method \think\Response expires(string $time) static expires
+ * @method \think\Response eTag(string $eTag) static eTag
+ * @method \think\Response cacheControl(string $cache) static 页面缓存控制
+ * @method \think\Response contentType(string $contentType, string $charset = 'utf-8') static 页面输出类型
+ * @method mixed getHeader(string $name) static 获取头部信息
+ * @method mixed getData() static 获取原始数据
+ * @method mixed getContent() static 获取输出数据
+ * @method int getCode() static 获取状态码
+ */
+class Response extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Route.php b/thinkphp/library/think/facade/Route.php
new file mode 100644
index 00000000..c9f843d9
--- /dev/null
+++ b/thinkphp/library/think/facade/Route.php
@@ -0,0 +1,48 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Route
+ * @mixin \think\Route
+ * @method \think\route\Domain domain(mixed $name, mixed $rule = '', array $option = [], array $pattern = []) static 注册域名路由
+ * @method \think\Route pattern(mixed $name, string $rule = '') static 注册变量规则
+ * @method \think\Route option(mixed $name, mixed $value = '') static 注册路由参数
+ * @method \think\Route bind(string $bind) static 设置路由绑定
+ * @method mixed getBind(string $bind) static 读取路由绑定
+ * @method \think\Route name(string $name) static 设置当前路由标识
+ * @method mixed getName(string $name) static 读取路由标识
+ * @method void setName(string $name) static 批量导入路由标识
+ * @method void import(array $rules, string $type = '*') static 导入配置文件的路由规则
+ * @method \think\route\RuleItem rule(string $rule, mixed $route, string $method = '*', array $option = [], array $pattern = []) static 注册路由规则
+ * @method void rules(array $rules, string $method = '*', array $option = [], array $pattern = []) static 批量注册路由规则
+ * @method \think\route\RuleGroup group(string|array $name, mixed $route, string $method = '*', array $option = [], array $pattern = []) static 注册路由分组
+ * @method \think\route\RuleItem any(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\RuleItem get(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\RuleItem post(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\RuleItem put(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\RuleItem delete(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\RuleItem patch(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册路由
+ * @method \think\route\Resource resource(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册资源路由
+ * @method \think\Route controller(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册控制器路由
+ * @method \think\Route alias(string $rule, mixed $route, array $option = [], array $pattern = []) static 注册别名路由
+ * @method \think\Route setMethodPrefix(mixed $method, string $prefix = '') static 设置不同请求类型下面的方法前缀
+ * @method \think\Route rest(string $name, array $resource = []) static rest方法定义和修改
+ * @method \think\Route\RuleItem miss(string $route, string $method = '*', array $option = []) static 注册未匹配路由规则后的处理
+ * @method \think\Route\RuleItem auto(string $route) static 注册一个自动解析的URL路由
+ * @method \think\Route\Dispatch check(string $url, string $depr = '/', bool $must = false, bool $completeMatch = false) static 检测URL路由
+ */
+class Route extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Session.php b/thinkphp/library/think/facade/Session.php
new file mode 100644
index 00000000..a68db382
--- /dev/null
+++ b/thinkphp/library/think/facade/Session.php
@@ -0,0 +1,37 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Session
+ * @mixin \think\Session
+ * @method void init(array $config = []) static session初始化
+ * @method bool has(string $name,string $prefix = null) static 判断session数据
+ * @method mixed prefix(string $prefix = '') static 设置或者获取session作用域(前缀)
+ * @method mixed get(string $name = '',string $prefix = null) static session获取
+ * @method mixed pull(string $name,string $prefix = null) static session获取并删除
+ * @method void push(string $key, mixed $value) static 添加数据到一个session数组
+ * @method void set(string $name, mixed $value , string $prefix = null) static 设置session数据
+ * @method void flash(string $name, mixed $value = null) static session设置 下一次请求有效
+ * @method void flush() static 清空当前请求的session数据
+ * @method void delete(string $name, string $prefix = null) static 删除session数据
+ * @method void clear($prefix = null) static 清空session数据
+ * @method void start() static 启动session
+ * @method void destroy() static 销毁session
+ * @method void pause() static 暂停session
+ * @method void regenerate(bool $delete = false) static 重新生成session_id
+ */
+class Session extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Url.php b/thinkphp/library/think/facade/Url.php
new file mode 100644
index 00000000..db5a16f2
--- /dev/null
+++ b/thinkphp/library/think/facade/Url.php
@@ -0,0 +1,24 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Url
+ * @mixin \think\Url
+ * @method string build(string $url = '', mixed $vars = '', mixed $suffix = true, mixed $domain = false) static URL生成 支持路由反射
+ * @method void root(string $root) static 指定当前生成URL地址的root
+ */
+class Url extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/Validate.php b/thinkphp/library/think/facade/Validate.php
new file mode 100644
index 00000000..423446d4
--- /dev/null
+++ b/thinkphp/library/think/facade/Validate.php
@@ -0,0 +1,65 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\Validate
+ * @mixin \think\Validate
+ * @method \think\Validate make(array $rules = [], array $message = [], array $field = []) static 创建一个验证器类
+ * @method \think\Validate rule(mixed $name, mixed $rule = '') static 添加字段验证规则
+ * @method void extend(string $type, mixed $callback = null) static 注册扩展验证(类型)规则
+ * @method void setTypeMsg(mixed $type, string $msg = null) static 设置验证规则的默认提示信息
+ * @method \think\Validate message(mixed $name, string $message = '') static 设置提示信息
+ * @method \think\Validate scene(string $name) static 设置验证场景
+ * @method bool hasScene(string $name) static 判断是否存在某个验证场景
+ * @method \think\Validate batch(bool $batch = true) static 设置批量验证
+ * @method \think\Validate only(array $fields) static 指定需要验证的字段列表
+ * @method \think\Validate remove(mixed $field, mixed $rule = true) static 移除某个字段的验证规则
+ * @method \think\Validate append(mixed $field, mixed $rule = null) static 追加某个字段的验证规则
+ * @method bool confirm(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否和某个字段的值一致
+ * @method bool different(mixed $value, mixed $rule, array $data = []) static 验证是否和某个字段的值是否不同
+ * @method bool egt(mixed $value, mixed $rule, array $data = []) static 验证是否大于等于某个值
+ * @method bool gt(mixed $value, mixed $rule, array $data = []) static 验证是否大于某个值
+ * @method bool elt(mixed $value, mixed $rule, array $data = []) static 验证是否小于等于某个值
+ * @method bool lt(mixed $value, mixed $rule, array $data = []) static 验证是否小于某个值
+ * @method bool eq(mixed $value, mixed $rule) static 验证是否等于某个值
+ * @method bool must(mixed $value, mixed $rule) static 必须验证
+ * @method bool is(mixed $value, mixed $rule, array $data = []) static 验证字段值是否为有效格式
+ * @method bool ip(mixed $value, mixed $rule) static 验证是否有效IP
+ * @method bool requireIf(mixed $value, mixed $rule) static 验证某个字段等于某个值的时候必须
+ * @method bool requireCallback(mixed $value, mixed $rule,array $data) static 通过回调方法验证某个字段是否必须
+ * @method bool requireWith(mixed $value, mixed $rule, array $data) static 验证某个字段有值的情况下必须
+ * @method bool filter(mixed $value, mixed $rule) static 使用filter_var方式验证
+ * @method bool in(mixed $value, mixed $rule) static 验证是否在范围内
+ * @method bool notIn(mixed $value, mixed $rule) static 验证是否不在范围内
+ * @method bool between(mixed $value, mixed $rule) static between验证数据
+ * @method bool notBetween(mixed $value, mixed $rule) static 使用notbetween验证数据
+ * @method bool length(mixed $value, mixed $rule) static 验证数据长度
+ * @method bool max(mixed $value, mixed $rule) static 验证数据最大长度
+ * @method bool min(mixed $value, mixed $rule) static 验证数据最小长度
+ * @method bool after(mixed $value, mixed $rule) static 验证日期
+ * @method bool before(mixed $value, mixed $rule) static 验证日期
+ * @method bool expire(mixed $value, mixed $rule) static 验证有效期
+ * @method bool allowIp(mixed $value, mixed $rule) static 验证IP许可
+ * @method bool denyIp(mixed $value, mixed $rule) static 验证IP禁用
+ * @method bool regex(mixed $value, mixed $rule) static 使用正则验证数据
+ * @method bool token(mixed $value, mixed $rule) static 验证表单令牌
+ * @method bool dateFormat(mixed $value, mixed $rule) static 验证时间和日期是否符合指定格式
+ * @method bool unique(mixed $value, mixed $rule, array $data = [], string $field = '') static 验证是否唯一
+ * @method bool check(array $data, mixed $rules = [], string $scene = '') static 数据自动验证
+ * @method mixed getError(mixed $value, mixed $rule) static 获取错误信息
+ */
+class Validate extends Facade
+{
+}
diff --git a/thinkphp/library/think/facade/View.php b/thinkphp/library/think/facade/View.php
new file mode 100644
index 00000000..0309a760
--- /dev/null
+++ b/thinkphp/library/think/facade/View.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+use think\Facade;
+
+/**
+ * @see \think\View
+ * @mixin \think\View
+ * @method \think\View init(mixed $engine = [], array $replace = []) static 初始化
+ * @method \think\View share(mixed $name, mixed $value = '') static 模板变量静态赋值
+ * @method \think\View assign(mixed $name, mixed $value = '') static 模板变量赋值
+ * @method \think\View config(mixed $name, mixed $value = '') static 配置模板引擎
+ * @method \think\View exists(mixed $name) static 检查模板是否存在
+ * @method \think\View filter(Callable $filter) static 视图内容过滤
+ * @method \think\View engine(mixed $engine = []) static 设置当前模板解析的引擎
+ * @method string fetch(string $template = '', array $vars = [], array $replace = [], array $config = [], bool $renderContent = false) static 解析和获取模板内容
+ * @method string display(string $content = '', array $vars = [], array $replace = [], array $config = []) static 渲染内容输出
+ */
+class View extends Facade
+{
+}
diff --git a/thinkphp/library/think/log/driver/File.php b/thinkphp/library/think/log/driver/File.php
new file mode 100644
index 00000000..dfd963c2
--- /dev/null
+++ b/thinkphp/library/think/log/driver/File.php
@@ -0,0 +1,280 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\log\driver;
+
+use think\App;
+
+/**
+ * 本地化调试输出到文件
+ */
+class File
+{
+ protected $config = [
+ 'time_format' => ' c ',
+ 'single' => false,
+ 'file_size' => 2097152,
+ 'path' => '',
+ 'apart_level' => [],
+ 'max_files' => 0,
+ 'json' => false,
+ ];
+
+ protected $app;
+
+ // 实例化并传入参数
+ public function __construct(App $app, $config = [])
+ {
+ $this->app = $app;
+
+ if (is_array($config)) {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ if (empty($this->config['path'])) {
+ $this->config['path'] = $this->app->getRuntimePath() . 'log' . DIRECTORY_SEPARATOR;
+ } elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) {
+ $this->config['path'] .= DIRECTORY_SEPARATOR;
+ }
+ }
+
+ /**
+ * 日志写入接口
+ * @access public
+ * @param array $log 日志信息
+ * @param bool $append 是否追加请求信息
+ * @return bool
+ */
+ public function save(array $log = [], $append = false)
+ {
+ $destination = $this->getMasterLogFile();
+
+ $path = dirname($destination);
+ !is_dir($path) && mkdir($path, 0755, true);
+
+ $info = [];
+
+ foreach ($log as $type => $val) {
+
+ foreach ($val as $msg) {
+ if (!is_string($msg)) {
+ $msg = var_export($msg, true);
+ }
+
+ $info[$type][] = $this->config['json'] ? $msg : '[ ' . $type . ' ] ' . $msg;
+ }
+
+ if (!$this->config['json'] && (true === $this->config['apart_level'] || in_array($type, $this->config['apart_level']))) {
+ // 独立记录的日志级别
+ $filename = $this->getApartLevelFile($path, $type);
+
+ $this->write($info[$type], $filename, true, $append);
+
+ unset($info[$type]);
+ }
+ }
+
+ if ($info) {
+ return $this->write($info, $destination, false, $append);
+ }
+
+ return true;
+ }
+
+ /**
+ * 日志写入
+ * @access protected
+ * @param array $message 日志信息
+ * @param string $destination 日志文件
+ * @param bool $apart 是否独立文件写入
+ * @param bool $append 是否追加请求信息
+ * @return bool
+ */
+ protected function write($message, $destination, $apart = false, $append = false)
+ {
+ // 检测日志文件大小,超过配置大小则备份日志文件重新生成
+ $this->checkLogSize($destination);
+
+ // 日志信息封装
+ $info['timestamp'] = date($this->config['time_format']);
+
+ foreach ($message as $type => $msg) {
+ $info[$type] = is_array($msg) ? implode("\r\n", $msg) : $msg;
+ }
+
+ if (PHP_SAPI == 'cli') {
+ $message = $this->parseCliLog($info);
+ } else {
+ // 添加调试日志
+ $this->getDebugLog($info, $append, $apart);
+
+ $message = $this->parseLog($info);
+ }
+
+ return error_log($message, 3, $destination);
+ }
+
+ /**
+ * 获取主日志文件名
+ * @access public
+ * @return string
+ */
+ protected function getMasterLogFile()
+ {
+ if ($this->config['single']) {
+ $name = is_string($this->config['single']) ? $this->config['single'] : 'single';
+
+ $destination = $this->config['path'] . $name . '.log';
+ } else {
+ $cli = PHP_SAPI == 'cli' ? '_cli' : '';
+
+ if ($this->config['max_files']) {
+ $filename = date('Ymd') . $cli . '.log';
+ $files = glob($this->config['path'] . '*.log');
+
+ try {
+ if (count($files) > $this->config['max_files']) {
+ unlink($files[0]);
+ }
+ } catch (\Exception $e) {
+ }
+ } else {
+ $filename = date('Ym') . DIRECTORY_SEPARATOR . date('d') . $cli . '.log';
+ }
+
+ $destination = $this->config['path'] . $filename;
+ }
+
+ return $destination;
+ }
+
+ /**
+ * 获取独立日志文件名
+ * @access public
+ * @param string $path 日志目录
+ * @param string $type 日志类型
+ * @return string
+ */
+ protected function getApartLevelFile($path, $type)
+ {
+ $cli = PHP_SAPI == 'cli' ? '_cli' : '';
+
+ if ($this->config['single']) {
+ $name = is_string($this->config['single']) ? $this->config['single'] : 'single';
+
+ $name .= '_' . $type;
+ } elseif ($this->config['max_files']) {
+ $name = date('Ymd') . '_' . $type . $cli;
+ } else {
+ $name = date('d') . '_' . $type . $cli;
+ }
+
+ return $path . DIRECTORY_SEPARATOR . $name . '.log';
+ }
+
+ /**
+ * 检查日志文件大小并自动生成备份文件
+ * @access protected
+ * @param string $destination 日志文件
+ * @return void
+ */
+ protected function checkLogSize($destination)
+ {
+ if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) {
+ try {
+ rename($destination, dirname($destination) . DIRECTORY_SEPARATOR . time() . '-' . basename($destination));
+ } catch (\Exception $e) {
+ }
+ }
+ }
+
+ /**
+ * CLI日志解析
+ * @access protected
+ * @param array $info 日志信息
+ * @return string
+ */
+ protected function parseCliLog($info)
+ {
+ if ($this->config['json']) {
+ $message = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n";
+ } else {
+ $now = $info['timestamp'];
+ unset($info['timestamp']);
+
+ $message = implode("\r\n", $info);
+
+ $message = "[{$now}]" . $message . "\r\n";
+ }
+
+ return $message;
+ }
+
+ /**
+ * 解析日志
+ * @access protected
+ * @param array $info 日志信息
+ * @return string
+ */
+ protected function parseLog($info)
+ {
+ $requestInfo = [
+ 'ip' => $this->app['request']->ip(),
+ 'method' => $this->app['request']->method(),
+ 'host' => $this->app['request']->host(),
+ 'uri' => $this->app['request']->url(),
+ ];
+
+ if ($this->config['json']) {
+ $info = $requestInfo + $info;
+ return json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\r\n";
+ }
+
+ array_unshift($info, "---------------------------------------------------------------\r\n[{$info['timestamp']}] {$requestInfo['ip']} {$requestInfo['method']} {$requestInfo['host']}{$requestInfo['uri']}");
+ unset($info['timestamp']);
+
+ return implode("\r\n", $info) . "\r\n";
+ }
+
+ protected function getDebugLog(&$info, $append, $apart)
+ {
+ if ($this->app->isDebug() && $append) {
+
+ if ($this->config['json']) {
+ // 获取基本信息
+ $runtime = round(microtime(true) - $this->app->getBeginTime(), 10);
+ $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';
+
+ $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 1024, 2);
+
+ $info = [
+ 'runtime' => number_format($runtime, 6) . 's',
+ 'reqs' => $reqs . 'req/s',
+ 'memory' => $memory_use . 'kb',
+ 'file' => count(get_included_files()),
+ ] + $info;
+
+ } elseif (!$apart) {
+ // 增加额外的调试信息
+ $runtime = round(microtime(true) - $this->app->getBeginTime(), 10);
+ $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';
+
+ $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 1024, 2);
+
+ $time_str = '[运行时间:' . number_format($runtime, 6) . 's] [吞吐率:' . $reqs . 'req/s]';
+ $memory_str = ' [内存消耗:' . $memory_use . 'kb]';
+ $file_load = ' [文件加载:' . count(get_included_files()) . ']';
+
+ array_unshift($info, $time_str . $memory_str . $file_load);
+ }
+ }
+ }
+}
diff --git a/thinkphp/library/think/log/driver/Socket.php b/thinkphp/library/think/log/driver/Socket.php
new file mode 100644
index 00000000..b4a2cadc
--- /dev/null
+++ b/thinkphp/library/think/log/driver/Socket.php
@@ -0,0 +1,277 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\log\driver;
+
+use think\App;
+
+/**
+ * github: https://github.com/luofei614/SocketLog
+ * @author luofei614
+ */
+class Socket
+{
+ public $port = 1116; //SocketLog 服务的http的端口号
+
+ protected $config = [
+ // socket服务器地址
+ 'host' => 'localhost',
+ // 是否显示加载的文件列表
+ 'show_included_files' => false,
+ // 日志强制记录到配置的client_id
+ 'force_client_ids' => [],
+ // 限制允许读取日志的client_id
+ 'allow_client_ids' => [],
+ ];
+
+ protected $css = [
+ 'sql' => 'color:#009bb4;',
+ 'sql_warn' => 'color:#009bb4;font-size:14px;',
+ 'error' => 'color:#f4006b;font-size:14px;',
+ 'page' => 'color:#40e2ff;background:#171717;',
+ 'big' => 'font-size:20px;color:red;',
+ ];
+
+ protected $allowForceClientIds = []; //配置强制推送且被授权的client_id
+ protected $app;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array $config 缓存参数
+ */
+ public function __construct(App $app, array $config = [])
+ {
+ $this->app = $app;
+
+ if (!empty($config)) {
+ $this->config = array_merge($this->config, $config);
+ }
+ }
+
+ /**
+ * 调试输出接口
+ * @access public
+ * @param array $log 日志信息
+ * @return bool
+ */
+ public function save(array $log = [], $append = false)
+ {
+ if (!$this->check()) {
+ return false;
+ }
+
+ $trace = [];
+
+ if ($this->app->isDebug()) {
+ $runtime = round(microtime(true) - $this->app->getBeginTime(), 10);
+ $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞';
+ $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]';
+ $memory_use = number_format((memory_get_usage() - $this->app->getBeginMem()) / 1024, 2);
+ $memory_str = ' [内存消耗:' . $memory_use . 'kb]';
+ $file_load = ' [文件加载:' . count(get_included_files()) . ']';
+
+ if (isset($_SERVER['HTTP_HOST'])) {
+ $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ } else {
+ $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']);
+ }
+
+ // 基本信息
+ $trace[] = [
+ 'type' => 'group',
+ 'msg' => $current_uri . $time_str . $memory_str . $file_load,
+ 'css' => $this->css['page'],
+ ];
+ }
+
+ foreach ($log as $type => $val) {
+ $trace[] = [
+ 'type' => 'groupCollapsed',
+ 'msg' => '[ ' . $type . ' ]',
+ 'css' => isset($this->css[$type]) ? $this->css[$type] : '',
+ ];
+
+ foreach ($val as $msg) {
+ if (!is_string($msg)) {
+ $msg = var_export($msg, true);
+ }
+ $trace[] = [
+ 'type' => 'log',
+ 'msg' => $msg,
+ 'css' => '',
+ ];
+ }
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+ }
+
+ if ($this->config['show_included_files']) {
+ $trace[] = [
+ 'type' => 'groupCollapsed',
+ 'msg' => '[ file ]',
+ 'css' => '',
+ ];
+
+ $trace[] = [
+ 'type' => 'log',
+ 'msg' => implode("\n", get_included_files()),
+ 'css' => '',
+ ];
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+ }
+
+ $trace[] = [
+ 'type' => 'groupEnd',
+ 'msg' => '',
+ 'css' => '',
+ ];
+
+ $tabid = $this->getClientArg('tabid');
+
+ if (!$client_id = $this->getClientArg('client_id')) {
+ $client_id = '';
+ }
+
+ if (!empty($this->allowForceClientIds)) {
+ //强制推送到多个client_id
+ foreach ($this->allowForceClientIds as $force_client_id) {
+ $client_id = $force_client_id;
+ $this->sendToClient($tabid, $client_id, $trace, $force_client_id);
+ }
+ } else {
+ $this->sendToClient($tabid, $client_id, $trace, '');
+ }
+
+ return true;
+ }
+
+ /**
+ * 发送给指定客户端
+ * @access protected
+ * @author Zjmainstay
+ * @param $tabid
+ * @param $client_id
+ * @param $logs
+ * @param $force_client_id
+ */
+ protected function sendToClient($tabid, $client_id, $logs, $force_client_id)
+ {
+ $logs = [
+ 'tabid' => $tabid,
+ 'client_id' => $client_id,
+ 'logs' => $logs,
+ 'force_client_id' => $force_client_id,
+ ];
+
+ $msg = @json_encode($logs);
+ $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁
+
+ $this->send($this->config['host'], $msg, $address);
+ }
+
+ protected function check()
+ {
+ $tabid = $this->getClientArg('tabid');
+
+ //是否记录日志的检查
+ if (!$tabid && !$this->config['force_client_ids']) {
+ return false;
+ }
+
+ //用户认证
+ $allow_client_ids = $this->config['allow_client_ids'];
+
+ if (!empty($allow_client_ids)) {
+ //通过数组交集得出授权强制推送的client_id
+ $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']);
+ if (!$tabid && count($this->allowForceClientIds)) {
+ return true;
+ }
+
+ $client_id = $this->getClientArg('client_id');
+ if (!in_array($client_id, $allow_client_ids)) {
+ return false;
+ }
+ } else {
+ $this->allowForceClientIds = $this->config['force_client_ids'];
+ }
+
+ return true;
+ }
+
+ protected function getClientArg($name)
+ {
+ static $args = [];
+
+ $key = 'HTTP_USER_AGENT';
+
+ if (isset($_SERVER['HTTP_SOCKETLOG'])) {
+ $key = 'HTTP_SOCKETLOG';
+ }
+
+ if (!isset($_SERVER[$key])) {
+ return;
+ }
+
+ if (empty($args)) {
+ if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) {
+ $args = ['tabid' => null];
+ return;
+ }
+ parse_str($match[1], $args);
+ }
+
+ if (isset($args[$name])) {
+ return $args[$name];
+ }
+
+ return;
+ }
+
+ /**
+ * @access protected
+ * @param string $host - $host of socket server
+ * @param string $message - 发送的消息
+ * @param string $address - 地址
+ * @return bool
+ */
+ protected function send($host, $message = '', $address = '/')
+ {
+ $url = 'http://' . $host . ':' . $this->port . $address;
+ $ch = curl_init();
+
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+ $headers = [
+ "Content-Type: application/json;charset=UTF-8",
+ ];
+
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header
+
+ return curl_exec($ch);
+ }
+
+}
diff --git a/thinkphp/library/think/model/Collection.php b/thinkphp/library/think/model/Collection.php
new file mode 100644
index 00000000..3a9b60f5
--- /dev/null
+++ b/thinkphp/library/think/model/Collection.php
@@ -0,0 +1,96 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model;
+
+use think\Collection as BaseCollection;
+use think\Model;
+
+class Collection extends BaseCollection
+{
+ /**
+ * 返回数组中指定的一列
+ * @access public
+ * @param string $column_key
+ * @param string|null $index_key
+ * @return array
+ */
+ public function column($column_key, $index_key = null)
+ {
+ return array_column($this->toArray(), $column_key, $index_key);
+ }
+
+ /**
+ * 延迟预载入关联查询
+ * @access public
+ * @param mixed $relation 关联
+ * @return $this
+ */
+ public function load($relation)
+ {
+ $item = current($this->items);
+ $item->eagerlyResultSet($this->items, $relation);
+
+ return $this;
+ }
+
+ /**
+ * 设置需要隐藏的输出属性
+ * @access public
+ * @param array $hidden 属性列表
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function hidden($hidden = [], $override = false)
+ {
+ $this->each(function ($model) use ($hidden, $override) {
+ /** @var Model $model */
+ $model->hidden($hidden, $override);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置需要输出的属性
+ * @access public
+ * @param array $visible
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function visible($visible = [], $override = false)
+ {
+ $this->each(function ($model) use ($visible, $override) {
+ /** @var Model $model */
+ $model->visible($visible, $override);
+ });
+
+ return $this;
+ }
+
+ /**
+ * 设置需要追加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function append($append = [], $override = false)
+ {
+ $this->each(function ($model) use ($append, $override) {
+ /** @var Model $model */
+ $model && $model->append($append, $override);
+ });
+
+ return $this;
+ }
+
+}
diff --git a/thinkphp/library/think/model/Pivot.php b/thinkphp/library/think/model/Pivot.php
new file mode 100644
index 00000000..a3a395e3
--- /dev/null
+++ b/thinkphp/library/think/model/Pivot.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model;
+
+use think\Model;
+
+class Pivot extends Model
+{
+
+ /** @var Model */
+ public $parent;
+
+ protected $autoWriteTimestamp = false;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param array|object $data 数据
+ * @param Model $parent 上级模型
+ * @param string $table 中间数据表名
+ */
+ public function __construct($data = [], Model $parent = null, $table = '')
+ {
+ $this->parent = $parent;
+
+ if (is_null($this->name)) {
+ $this->name = $table;
+ }
+
+ parent::__construct($data);
+ }
+
+}
diff --git a/thinkphp/library/think/model/Relation.php b/thinkphp/library/think/model/Relation.php
new file mode 100644
index 00000000..b969bca2
--- /dev/null
+++ b/thinkphp/library/think/model/Relation.php
@@ -0,0 +1,164 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model;
+
+use think\db\Query;
+use think\Exception;
+use think\Model;
+
+/**
+ * Class Relation
+ * @package think\model
+ *
+ * @mixin Query
+ */
+abstract class Relation
+{
+ // 父模型对象
+ protected $parent;
+ /** @var Model 当前关联的模型类 */
+ protected $model;
+ /** @var Query 关联模型查询对象 */
+ protected $query;
+ // 关联表外键
+ protected $foreignKey;
+ // 关联表主键
+ protected $localKey;
+ // 基础查询
+ protected $baseQuery;
+ // 是否为自关联
+ protected $selfRelation;
+
+ /**
+ * 获取关联的所属模型
+ * @access public
+ * @return Model
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @access public
+ * @return Model
+ */
+ public function getModel()
+ {
+ return $this->query->getModel();
+ }
+
+ /**
+ * 设置当前关联为自关联
+ * @access public
+ * @param bool $self 是否自关联
+ * @return $this
+ */
+ public function selfRelation($self = true)
+ {
+ $this->selfRelation = $self;
+ return $this;
+ }
+
+ /**
+ * 当前关联是否为自关联
+ * @access public
+ * @return bool
+ */
+ public function isSelfRelation()
+ {
+ return $this->selfRelation;
+ }
+
+ /**
+ * 封装关联数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @return mixed
+ */
+ protected function resultSetBuild($resultSet)
+ {
+ return (new $this->model)->toCollection($resultSet);
+ }
+
+ protected function getQueryFields($model)
+ {
+ $fields = $this->query->getOptions('field');
+ return $this->getRelationQueryFields($fields, $model);
+ }
+
+ protected function getRelationQueryFields($fields, $model)
+ {
+ if ($fields) {
+
+ if (is_string($fields)) {
+ $fields = explode(',', $fields);
+ }
+
+ foreach ($fields as &$field) {
+ if (false === strpos($field, '.')) {
+ $field = $model . '.' . $field;
+ }
+ }
+ } else {
+ $fields = $model . '.*';
+ }
+
+ return $fields;
+ }
+
+ protected function getQueryWhere(&$where, $relation)
+ {
+ foreach ($where as $key => $val) {
+ if (is_string($key)) {
+ $where[] = [false === strpos($key, '.') ? $relation . '.' . $key : $key, '=', $val];
+ unset($where[$key]);
+ }
+ }
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 表达式 true 表示强制删除
+ * @return int
+ * @throws Exception
+ * @throws PDOException
+ */
+ public function delete($data = null)
+ {
+ return $this->query->delete($data);
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {}
+
+ public function __call($method, $args)
+ {
+ if ($this->query) {
+ // 执行基础查询
+ $this->baseQuery();
+
+ $result = call_user_func_array([$this->query->getModel(), $method], $args);
+
+ return $result === $this->query ? $this : $result;
+ } else {
+ throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/concern/Attribute.php b/thinkphp/library/think/model/concern/Attribute.php
new file mode 100644
index 00000000..c65c557b
--- /dev/null
+++ b/thinkphp/library/think/model/concern/Attribute.php
@@ -0,0 +1,586 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\concern;
+
+use InvalidArgumentException;
+use think\Exception;
+use think\Loader;
+use think\model\Relation;
+
+trait Attribute
+{
+ /**
+ * 数据表主键 复合主键使用数组定义
+ * @var string|array
+ */
+ protected $pk = 'id';
+
+ /**
+ * 数据表字段信息 留空则自动获取
+ * @var array
+ */
+ protected $field = [];
+
+ /**
+ * JSON数据表字段
+ * @var array
+ */
+ protected $json = [];
+
+ /**
+ * JSON数据取出是否需要转换为数组
+ * @var bool
+ */
+ protected $jsonAssoc = false;
+
+ /**
+ * JSON数据表字段类型
+ * @var array
+ */
+ protected $jsonType = [];
+
+ /**
+ * 数据表废弃字段
+ * @var array
+ */
+ protected $disuse = [];
+
+ /**
+ * 数据表只读字段
+ * @var array
+ */
+ protected $readonly = [];
+
+ /**
+ * 数据表字段类型
+ * @var array
+ */
+ protected $type = [];
+
+ /**
+ * 当前模型数据
+ * @var array
+ */
+ private $data = [];
+
+ /**
+ * 原始数据
+ * @var array
+ */
+ private $origin = [];
+
+ /**
+ * 获取模型对象的主键
+ * @access public
+ * @return string|array
+ */
+ public function getPk()
+ {
+ return $this->pk;
+ }
+
+ /**
+ * 判断一个字段名是否为主键字段
+ * @access public
+ * @param string $key 名称
+ * @return bool
+ */
+ protected function isPk($key)
+ {
+ $pk = $this->getPk();
+ if (is_string($pk) && $pk == $key) {
+ return true;
+ } elseif (is_array($pk) && in_array($key, $pk)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 设置允许写入的字段
+ * @access public
+ * @param array|string|true $field 允许写入的字段 如果为true只允许写入数据表字段
+ * @return $this
+ */
+ public function allowField($field)
+ {
+ if (is_string($field)) {
+ $field = explode(',', $field);
+ }
+
+ $this->field = $field;
+
+ return $this;
+ }
+
+ /**
+ * 设置只读字段
+ * @access public
+ * @param array|string $field 只读字段
+ * @return $this
+ */
+ public function readonly($field)
+ {
+ if (is_string($field)) {
+ $field = explode(',', $field);
+ }
+
+ $this->readonly = $field;
+
+ return $this;
+ }
+
+ /**
+ * 设置数据对象值
+ * @access public
+ * @param mixed $data 数据或者属性名
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function data($data, $value = null)
+ {
+ if (is_string($data)) {
+ $this->data[$data] = $value;
+ return $this;
+ }
+
+ // 清空数据
+ $this->data = [];
+
+ if (is_object($data)) {
+ $data = get_object_vars($data);
+ }
+
+ if ($this->disuse) {
+ // 废弃字段
+ foreach ((array) $this->disuse as $key) {
+ if (array_key_exists($key, $data)) {
+ unset($data[$key]);
+ }
+ }
+ }
+
+ if (true === $value) {
+ // 数据对象赋值
+ foreach ($data as $key => $value) {
+ $this->setAttr($key, $value, $data);
+ }
+ } elseif (is_array($value)) {
+ foreach ($value as $name) {
+ if (isset($data[$name])) {
+ $this->data[$name] = $data[$name];
+ }
+ }
+ } else {
+ $this->data = $data;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 批量设置数据对象值
+ * @access public
+ * @param mixed $data 数据
+ * @param bool $set 是否需要进行数据处理
+ * @return $this
+ */
+ public function appendData($data, $set = false)
+ {
+ if ($set) {
+ // 进行数据处理
+ foreach ($data as $key => $value) {
+ $this->setAttr($key, $value, $data);
+ }
+ } else {
+ if (is_object($data)) {
+ $data = get_object_vars($data);
+ }
+
+ $this->data = array_merge($this->data, $data);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取对象原始数据 如果不存在指定字段返回null
+ * @access public
+ * @param string $name 字段名 留空获取全部
+ * @return mixed
+ */
+ public function getOrigin($name = null)
+ {
+ if (is_null($name)) {
+ return $this->origin;
+ }
+ return array_key_exists($name, $this->origin) ? $this->origin[$name] : null;
+ }
+
+ /**
+ * 获取对象原始数据 如果不存在指定字段返回false
+ * @access public
+ * @param string $name 字段名 留空获取全部
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ public function getData($name = null)
+ {
+ if (is_null($name)) {
+ return $this->data;
+ } elseif (array_key_exists($name, $this->data)) {
+ return $this->data[$name];
+ } elseif (array_key_exists($name, $this->relation)) {
+ return $this->relation[$name];
+ }
+ throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
+ }
+
+ /**
+ * 获取变化的数据 并排除只读数据
+ * @access public
+ * @return array
+ */
+ public function getChangedData()
+ {
+ if ($this->force) {
+ $data = $this->data;
+ } else {
+ $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
+ if ((empty($a) || empty($b)) && $a !== $b) {
+ return 1;
+ }
+
+ return is_object($a) || $a != $b ? 1 : 0;
+ });
+ }
+
+ if (!empty($this->readonly)) {
+ // 只读字段不允许更新
+ foreach ($this->readonly as $key => $field) {
+ if (isset($data[$field])) {
+ unset($data[$field]);
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 修改器 设置数据对象值
+ * @access public
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
+ * @return $this
+ */
+ public function setAttr($name, $value, $data = [])
+ {
+ if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
+ // 自动写入的时间戳字段
+ $value = $this->autoWriteTimestamp($name);
+ } else {
+ // 检测修改器
+ $method = 'set' . Loader::parseName($name, 1) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ $value = $this->$method($value, array_merge($this->data, $data));
+ } elseif (isset($this->type[$name])) {
+ // 类型转换
+ $value = $this->writeTransform($value, $this->type[$name]);
+ }
+ }
+
+ // 设置数据对象属性
+ $this->data[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 是否需要自动写入时间字段
+ * @access public
+ * @param bool $auto
+ * @return $this
+ */
+ public function isAutoWriteTimestamp($auto)
+ {
+ $this->autoWriteTimestamp = $auto;
+
+ return $this;
+ }
+
+ /**
+ * 自动写入时间戳
+ * @access protected
+ * @param string $name 时间戳字段
+ * @return mixed
+ */
+ protected function autoWriteTimestamp($name)
+ {
+ if (isset($this->type[$name])) {
+ $type = $this->type[$name];
+
+ if (strpos($type, ':')) {
+ list($type, $param) = explode(':', $type, 2);
+ }
+
+ switch ($type) {
+ case 'datetime':
+ case 'date':
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = $this->formatDateTime(time(), $format);
+ break;
+ case 'timestamp':
+ case 'integer':
+ default:
+ $value = time();
+ break;
+ }
+ } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
+ 'datetime',
+ 'date',
+ 'timestamp',
+ ])) {
+ $value = $this->formatDateTime(time(), $this->dateFormat);
+ } else {
+ $value = $this->formatDateTime(time(), $this->dateFormat, true);
+ }
+
+ return $value;
+ }
+
+ /**
+ * 数据写入 类型转换
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
+ * @return mixed
+ */
+ protected function writeTransform($value, $type)
+ {
+ if (is_null($value)) {
+ return;
+ }
+
+ if (is_array($type)) {
+ list($type, $param) = $type;
+ } elseif (strpos($type, ':')) {
+ list($type, $param) = explode(':', $type, 2);
+ }
+
+ switch ($type) {
+ case 'integer':
+ $value = (int) $value;
+ break;
+ case 'float':
+ if (empty($param)) {
+ $value = (float) $value;
+ } else {
+ $value = (float) number_format($value, $param, '.', '');
+ }
+ break;
+ case 'boolean':
+ $value = (bool) $value;
+ break;
+ case 'timestamp':
+ if (!is_numeric($value)) {
+ $value = strtotime($value);
+ }
+ break;
+ case 'datetime':
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = is_numeric($value) ? $value : strtotime($value);
+ $value = $this->formatDateTime($value, $format);
+ break;
+ case 'object':
+ if (is_object($value)) {
+ $value = json_encode($value, JSON_FORCE_OBJECT);
+ }
+ break;
+ case 'array':
+ $value = (array) $value;
+ case 'json':
+ $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
+ $value = json_encode($value, $option);
+ break;
+ case 'serialize':
+ $value = serialize($value);
+ break;
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取器 获取数据对象的值
+ * @access public
+ * @param string $name 名称
+ * @param array $item 数据
+ * @return mixed
+ * @throws InvalidArgumentException
+ */
+ public function getAttr($name, &$item = null)
+ {
+ try {
+ $notFound = false;
+ $value = $this->getData($name);
+ } catch (InvalidArgumentException $e) {
+ $notFound = true;
+ $value = null;
+ }
+
+ // 检测属性获取器
+ $method = 'get' . Loader::parseName($name, 1) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ if ($notFound && $relation = $this->isRelationAttr($name)) {
+ $modelRelation = $this->$relation();
+ $value = $this->getRelationData($modelRelation);
+ }
+
+ $value = $this->$method($value, $this->data);
+ } elseif (isset($this->type[$name])) {
+ // 类型转换
+ $value = $this->readTransform($value, $this->type[$name]);
+ } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
+ if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
+ 'datetime',
+ 'date',
+ 'timestamp',
+ ])) {
+ $value = $this->formatDateTime(strtotime($value), $this->dateFormat);
+ } else {
+ $value = $this->formatDateTime($value, $this->dateFormat);
+ }
+ } elseif ($notFound) {
+ $value = $this->getRelationAttribute($name, $item);
+ }
+
+ return $value;
+ }
+
+ /**
+ * 获取关联属性值
+ * @access protected
+ * @param string $name 属性名
+ * @param array $item 数据
+ * @return mixed
+ */
+ protected function getRelationAttribute($name, &$item)
+ {
+ $relation = $this->isRelationAttr($name);
+
+ if ($relation) {
+ $modelRelation = $this->$relation();
+ if ($modelRelation instanceof Relation) {
+ $value = $this->getRelationData($modelRelation);
+
+ if ($item && method_exists($modelRelation, 'getBindAttr') && $bindAttr = $modelRelation->getBindAttr()) {
+
+ foreach ($bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+
+ if (isset($item[$key])) {
+ throw new Exception('bind attr has exists:' . $key);
+ } else {
+ $item[$key] = $value ? $value->getAttr($attr) : null;
+ }
+ }
+
+ return false;
+ }
+
+ // 保存关联对象值
+ $this->relation[$name] = $value;
+
+ return $value;
+ }
+ }
+
+ throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
+ }
+
+ /**
+ * 数据读取 类型转换
+ * @access protected
+ * @param mixed $value 值
+ * @param string|array $type 要转换的类型
+ * @return mixed
+ */
+ protected function readTransform($value, $type)
+ {
+ if (is_null($value)) {
+ return;
+ }
+
+ if (is_array($type)) {
+ list($type, $param) = $type;
+ } elseif (strpos($type, ':')) {
+ list($type, $param) = explode(':', $type, 2);
+ }
+
+ switch ($type) {
+ case 'integer':
+ $value = (int) $value;
+ break;
+ case 'float':
+ if (empty($param)) {
+ $value = (float) $value;
+ } else {
+ $value = (float) number_format($value, $param, '.', '');
+ }
+ break;
+ case 'boolean':
+ $value = (bool) $value;
+ break;
+ case 'timestamp':
+ if (!is_null($value)) {
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = $this->formatDateTime($value, $format);
+ }
+ break;
+ case 'datetime':
+ if (!is_null($value)) {
+ $format = !empty($param) ? $param : $this->dateFormat;
+ $value = $this->formatDateTime(strtotime($value), $format);
+ }
+ break;
+ case 'json':
+ $value = json_decode($value, true);
+ break;
+ case 'array':
+ $value = empty($value) ? [] : json_decode($value, true);
+ break;
+ case 'object':
+ $value = empty($value) ? new \stdClass() : json_decode($value);
+ break;
+ case 'serialize':
+ try {
+ $value = unserialize($value);
+ } catch (\Exception $e) {
+ $value = null;
+ }
+ break;
+ default:
+ if (false !== strpos($type, '\\')) {
+ // 对象类型
+ $value = new $type($value);
+ }
+ }
+
+ return $value;
+ }
+
+}
diff --git a/thinkphp/library/think/model/concern/Conversion.php b/thinkphp/library/think/model/concern/Conversion.php
new file mode 100644
index 00000000..b88528ad
--- /dev/null
+++ b/thinkphp/library/think/model/concern/Conversion.php
@@ -0,0 +1,288 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\concern;
+
+use think\Collection;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Collection as ModelCollection;
+
+/**
+ * 模型数据转换处理
+ */
+trait Conversion
+{
+ /**
+ * 数据输出显示的属性
+ * @var array
+ */
+ protected $visible = [];
+
+ /**
+ * 数据输出隐藏的属性
+ * @var array
+ */
+ protected $hidden = [];
+
+ /**
+ * 数据输出需要追加的属性
+ * @var array
+ */
+ protected $append = [];
+
+ /**
+ * 数据集对象名
+ * @var string
+ */
+ protected $resultSetType;
+
+ /**
+ * 设置需要附加的输出属性
+ * @access public
+ * @param array $append 属性列表
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function append(array $append = [], $override = false)
+ {
+ $this->append = $override ? $append : array_merge($this->append, $append);
+
+ return $this;
+ }
+
+ /**
+ * 设置附加关联对象的属性
+ * @access public
+ * @param string $attr 关联属性
+ * @param string|array $append 追加属性名
+ * @return $this
+ * @throws Exception
+ */
+ public function appendRelationAttr($attr, $append)
+ {
+ if (is_string($append)) {
+ $append = explode(',', $append);
+ }
+
+ $relation = Loader::parseName($attr, 1, false);
+ if (isset($this->relation[$relation])) {
+ $model = $this->relation[$relation];
+ } else {
+ $model = $this->getRelationData($this->$relation());
+ }
+
+ if ($model instanceof Model) {
+ foreach ($append as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ if (isset($this->data[$key])) {
+ throw new Exception('bind attr has exists:' . $key);
+ } else {
+ $this->data[$key] = $model->$attr;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置需要隐藏的输出属性
+ * @access public
+ * @param array $hidden 属性列表
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function hidden(array $hidden = [], $override = false)
+ {
+ $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden);
+
+ return $this;
+ }
+
+ /**
+ * 设置需要输出的属性
+ * @access public
+ * @param array $visible
+ * @param bool $override 是否覆盖
+ * @return $this
+ */
+ public function visible(array $visible = [], $override = false)
+ {
+ $this->visible = $override ? $visible : array_merge($this->visible, $visible);
+
+ return $this;
+ }
+
+ /**
+ * 转换当前模型对象为数组
+ * @access public
+ * @return array
+ */
+ public function toArray()
+ {
+ $item = [];
+ $visible = [];
+ $hidden = [];
+
+ // 合并关联数据
+ $data = array_merge($this->data, $this->relation);
+
+ // 过滤属性
+ if (!empty($this->visible)) {
+ $array = $this->parseAttr($this->visible, $visible);
+ $data = array_intersect_key($data, array_flip($array));
+ } elseif (!empty($this->hidden)) {
+ $array = $this->parseAttr($this->hidden, $hidden, false);
+ $data = array_diff_key($data, array_flip($array));
+ }
+
+ foreach ($data as $key => $val) {
+ if ($val instanceof Model || $val instanceof ModelCollection) {
+ // 关联模型对象
+ if (isset($visible[$key])) {
+ $val->visible($visible[$key]);
+ } elseif (isset($hidden[$key])) {
+ $val->hidden($hidden[$key]);
+ }
+ // 关联模型对象
+ $item[$key] = $val->toArray();
+ } else {
+ // 模型属性
+ $item[$key] = $this->getAttr($key);
+ }
+ }
+
+ // 追加属性(必须定义获取器)
+ if (!empty($this->append)) {
+ foreach ($this->append as $key => $name) {
+ if (is_array($name)) {
+ // 追加关联对象属性
+ $relation = $this->getRelation($key);
+
+ if (!$relation) {
+ $relation = $this->getAttr($key);
+ $relation->visible($name);
+ }
+
+ $item[$key] = $relation->append($name)->toArray();
+ } elseif (strpos($name, '.')) {
+ list($key, $attr) = explode('.', $name);
+ // 追加关联对象属性
+ $relation = $this->getRelation($key);
+
+ if (!$relation) {
+ $relation = $this->getAttr($key);
+ $relation->visible([$attr]);
+ }
+
+ $item[$key] = $relation->append([$attr])->toArray();
+ } else {
+ $value = $this->getAttr($name, $item);
+ if (false !== $value) {
+ $item[$name] = $value;
+ }
+ }
+ }
+ }
+
+ return $item;
+ }
+
+ /**
+ * 转换当前模型对象为JSON字符串
+ * @access public
+ * @param integer $options json参数
+ * @return string
+ */
+ public function toJson($options = JSON_UNESCAPED_UNICODE)
+ {
+ return json_encode($this->toArray(), $options);
+ }
+
+ /**
+ * 移除当前模型的关联属性
+ * @access public
+ * @return $this
+ */
+ public function removeRelation()
+ {
+ $this->relation = [];
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->toJson();
+ }
+
+ // JsonSerializable
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * 转换数据集为数据集对象
+ * @access public
+ * @param array|Collection $collection 数据集
+ * @param string $resultSetType 数据集类
+ * @return Collection
+ */
+ public function toCollection($collection, $resultSetType = null)
+ {
+ $resultSetType = $resultSetType ?: $this->resultSetType;
+
+ if ($resultSetType && false !== strpos($resultSetType, '\\')) {
+ $collection = new $resultSetType($collection);
+ } else {
+ $collection = new ModelCollection($collection);
+ }
+
+ return $collection;
+ }
+
+ /**
+ * 解析隐藏及显示属性
+ * @access protected
+ * @param array $attrs 属性
+ * @param array $result 结果集
+ * @param bool $visible
+ * @return array
+ */
+ protected function parseAttr($attrs, &$result, $visible = true)
+ {
+ $array = [];
+
+ foreach ($attrs as $key => $val) {
+ if (is_array($val)) {
+ if ($visible) {
+ $array[] = $key;
+ }
+
+ $result[$key] = $val;
+ } elseif (strpos($val, '.')) {
+ list($key, $name) = explode('.', $val);
+
+ if ($visible) {
+ $array[] = $key;
+ }
+
+ $result[$key][] = $name;
+ } else {
+ $array[] = $val;
+ }
+ }
+
+ return $array;
+ }
+}
diff --git a/thinkphp/library/think/model/concern/ModelEvent.php b/thinkphp/library/think/model/concern/ModelEvent.php
new file mode 100644
index 00000000..2bb6d5fe
--- /dev/null
+++ b/thinkphp/library/think/model/concern/ModelEvent.php
@@ -0,0 +1,236 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\concern;
+
+use think\Container;
+use think\Loader;
+
+/**
+ * 模型事件处理
+ */
+trait ModelEvent
+{
+ /**
+ * 模型回调
+ * @var array
+ */
+ private static $event = [];
+
+ /**
+ * 模型事件观察
+ * @var array
+ */
+ protected static $observe = ['before_write', 'after_write', 'before_insert', 'after_insert', 'before_update', 'after_update', 'before_delete', 'after_delete', 'before_restore', 'after_restore'];
+
+ /**
+ * 绑定模型事件观察者类
+ * @var array
+ */
+ protected $observerClass;
+
+ /**
+ * 是否需要事件响应
+ * @var bool
+ */
+ private $withEvent = true;
+
+ /**
+ * 注册回调方法
+ * @access public
+ * @param string $event 事件名
+ * @param callable $callback 回调方法
+ * @param bool $override 是否覆盖
+ * @return void
+ */
+ public static function event($event, $callback, $override = false)
+ {
+ $class = static::class;
+
+ if ($override) {
+ self::$event[$class][$event] = [];
+ }
+
+ self::$event[$class][$event][] = $callback;
+ }
+
+ /**
+ * 清除回调方法
+ * @access public
+ * @return void
+ */
+ public static function flushEvent()
+ {
+ self::$event[static::class] = [];
+ }
+
+ /**
+ * 注册一个模型观察者
+ *
+ * @param object|string $class
+ * @return void
+ */
+ public static function observe($class)
+ {
+ foreach (static::$observe as $event) {
+ $eventFuncName = Loader::parseName($event, 1, false);
+
+ if (method_exists($class, $eventFuncName)) {
+ static::event($event, [$class, $eventFuncName]);
+ }
+ }
+ }
+
+ /**
+ * 当前操作的事件响应
+ * @access protected
+ * @param bool $event 是否需要事件响应
+ * @return $this
+ */
+ public function withEvent($event)
+ {
+ $this->withEvent = $event;
+ return $this;
+ }
+
+ /**
+ * 触发事件
+ * @access protected
+ * @param string $event 事件名
+ * @return bool
+ */
+ protected function trigger($event)
+ {
+ $class = static::class;
+
+ if ($this->withEvent && isset(self::$event[$class][$event])) {
+ foreach (self::$event[$class][$event] as $callback) {
+ $result = Container::getInstance()->invoke($callback, [$this]);
+
+ if (false === $result) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 模型before_insert事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function beforeInsert($callback, $override = false)
+ {
+ self::event('before_insert', $callback, $override);
+ }
+
+ /**
+ * 模型after_insert事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function afterInsert($callback, $override = false)
+ {
+ self::event('after_insert', $callback, $override);
+ }
+
+ /**
+ * 模型before_update事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function beforeUpdate($callback, $override = false)
+ {
+ self::event('before_update', $callback, $override);
+ }
+
+ /**
+ * 模型after_update事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function afterUpdate($callback, $override = false)
+ {
+ self::event('after_update', $callback, $override);
+ }
+
+ /**
+ * 模型before_write事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function beforeWrite($callback, $override = false)
+ {
+ self::event('before_write', $callback, $override);
+ }
+
+ /**
+ * 模型after_write事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function afterWrite($callback, $override = false)
+ {
+ self::event('after_write', $callback, $override);
+ }
+
+ /**
+ * 模型before_delete事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function beforeDelete($callback, $override = false)
+ {
+ self::event('before_delete', $callback, $override);
+ }
+
+ /**
+ * 模型after_delete事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function afterDelete($callback, $override = false)
+ {
+ self::event('after_delete', $callback, $override);
+ }
+
+ /**
+ * 模型before_restore事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function beforeRestore($callback, $override = false)
+ {
+ self::event('before_restore', $callback, $override);
+ }
+
+ /**
+ * 模型after_restore事件快捷方法
+ * @access protected
+ * @param callable $callback
+ * @param bool $override
+ */
+ protected static function afterRestore($callback, $override = false)
+ {
+ self::event('after_restore', $callback, $override);
+ }
+}
diff --git a/thinkphp/library/think/model/concern/RelationShip.php b/thinkphp/library/think/model/concern/RelationShip.php
new file mode 100644
index 00000000..2c993bd2
--- /dev/null
+++ b/thinkphp/library/think/model/concern/RelationShip.php
@@ -0,0 +1,643 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\concern;
+
+use think\Collection;
+use think\db\Query;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+use think\model\relation\BelongsTo;
+use think\model\relation\BelongsToMany;
+use think\model\relation\HasMany;
+use think\model\relation\HasManyThrough;
+use think\model\relation\HasOne;
+use think\model\relation\MorphMany;
+use think\model\relation\MorphOne;
+use think\model\relation\MorphTo;
+
+/**
+ * 模型关联处理
+ */
+trait RelationShip
+{
+ /**
+ * 父关联模型对象
+ * @var object
+ */
+ private $parent;
+
+ /**
+ * 模型关联数据
+ * @var array
+ */
+ private $relation = [];
+
+ /**
+ * 关联写入定义信息
+ * @var array
+ */
+ private $together;
+
+ /**
+ * 关联自动写入信息
+ * @var array
+ */
+ protected $relationWrite;
+
+ /**
+ * 设置父关联对象
+ * @access public
+ * @param Model $model 模型对象
+ * @return $this
+ */
+ public function setParent($model)
+ {
+ $this->parent = $model;
+
+ return $this;
+ }
+
+ /**
+ * 获取父关联对象
+ * @access public
+ * @return Model
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取当前模型的关联模型数据
+ * @access public
+ * @param string $name 关联方法名
+ * @return mixed
+ */
+ public function getRelation($name = null)
+ {
+ if (is_null($name)) {
+ return $this->relation;
+ } elseif (array_key_exists($name, $this->relation)) {
+ return $this->relation[$name];
+ }
+ return;
+ }
+
+ /**
+ * 设置关联数据对象值
+ * @access public
+ * @param string $name 属性名
+ * @param mixed $value 属性值
+ * @param array $data 数据
+ * @return $this
+ */
+ public function setRelation($name, $value, $data = [])
+ {
+ // 检测修改器
+ $method = 'set' . Loader::parseName($name, 1) . 'Attr';
+
+ if (method_exists($this, $method)) {
+ $value = $this->$method($value, array_merge($this->data, $data));
+ }
+
+ $this->relation[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 关联数据写入
+ * @access public
+ * @param array|string $relation 关联
+ * @return $this
+ */
+ public function together($relation)
+ {
+ if (is_string($relation)) {
+ $relation = explode(',', $relation);
+ }
+
+ $this->together = $relation;
+
+ $this->checkAutoRelationWrite();
+
+ return $this;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public static function has($relation, $operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ $relation = (new static())->$relation();
+
+ if (is_array($operator) || $operator instanceof \Closure) {
+ return $relation->hasWhere($operator);
+ }
+
+ return $relation->has($operator, $count, $id, $joinType);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $relation 关联方法名
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public static function hasWhere($relation, $where = [], $fields = '*')
+ {
+ return (new static())->$relation()->hasWhere($where, $fields);
+ }
+
+ /**
+ * 查询当前模型的关联数据
+ * @access public
+ * @param string|array $relations 关联名
+ * @return $this
+ */
+ public function relationQuery($relations)
+ {
+ if (is_string($relations)) {
+ $relations = explode(',', $relations);
+ }
+
+ foreach ($relations as $key => $relation) {
+ $subRelation = '';
+ $closure = null;
+
+ if ($relation instanceof \Closure) {
+ // 支持闭包查询过滤关联条件
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ list($relation, $subRelation) = explode('.', $relation, 2);
+ }
+
+ $method = Loader::parseName($relation, 1, false);
+
+ $this->relation[$relation] = $this->$method()->getRelation($subRelation, $closure);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询 返回数据集
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 关联名
+ * @return array
+ */
+ public function eagerlyResultSet(&$resultSet, $relation)
+ {
+ $relations = is_string($relation) ? explode(',', $relation) : $relation;
+
+ foreach ($relations as $key => $relation) {
+ $subRelation = '';
+ $closure = false;
+
+ if ($relation instanceof \Closure) {
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ list($relation, $subRelation) = explode('.', $relation, 2);
+ }
+
+ $relation = Loader::parseName($relation, 1, false);
+
+ $this->$relation()->eagerlyResultSet($resultSet, $relation, $subRelation, $closure);
+ }
+ }
+
+ /**
+ * 预载入关联查询 返回模型对象
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 关联名
+ * @return Model
+ */
+ public function eagerlyResult(&$result, $relation)
+ {
+ $relations = is_string($relation) ? explode(',', $relation) : $relation;
+
+ foreach ($relations as $key => $relation) {
+ $subRelation = '';
+ $closure = false;
+
+ if ($relation instanceof \Closure) {
+ $closure = $relation;
+ $relation = $key;
+ }
+
+ if (is_array($relation)) {
+ $subRelation = $relation;
+ $relation = $key;
+ } elseif (strpos($relation, '.')) {
+ list($relation, $subRelation) = explode('.', $relation, 2);
+ }
+
+ $relation = Loader::parseName($relation, 1, false);
+
+ $this->$relation()->eagerlyResult($result, $relation, $subRelation, $closure);
+ }
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param array $relations 关联名
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return void
+ */
+ public function relationCount(&$result, $relations, $aggregate = 'sum', $field = '*')
+ {
+ foreach ($relations as $key => $relation) {
+ $closure = false;
+
+ if ($relation instanceof \Closure) {
+ $closure = $relation;
+ $relation = $key;
+ } elseif (is_string($key)) {
+ $name = $relation;
+ $relation = $key;
+ }
+
+ $relation = Loader::parseName($relation, 1, false);
+ $count = $this->$relation()->relationCount($result, $closure, $aggregate, $field);
+
+ if (!isset($name)) {
+ $name = Loader::parseName($relation) . '_' . $aggregate;
+ }
+
+ $result->setAttr($name, $count);
+ }
+ }
+
+ /**
+ * HAS ONE 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
+ * @return HasOne
+ */
+ public function hasOne($model, $foreignKey = '', $localKey = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new HasOne($this, $model, $foreignKey, $localKey);
+ }
+
+ /**
+ * BELONGS TO 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
+ * @return BelongsTo
+ */
+ public function belongsTo($model, $foreignKey = '', $localKey = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $foreignKey = $foreignKey ?: $this->getForeignKey((new $model)->getName());
+ $localKey = $localKey ?: (new $model)->getPk();
+ $trace = debug_backtrace(false, 2);
+ $relation = Loader::parseName($trace[1]['function']);
+
+ return new BelongsTo($this, $model, $foreignKey, $localKey, $relation);
+ }
+
+ /**
+ * HAS MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前主键
+ * @return HasMany
+ */
+ public function hasMany($model, $foreignKey = '', $localKey = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+
+ return new HasMany($this, $model, $foreignKey, $localKey);
+ }
+
+ /**
+ * HAS MANY 远程关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ * @return HasManyThrough
+ */
+ public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $through = $this->parseModel($through);
+ $localKey = $localKey ?: $this->getPk();
+ $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+ $throughKey = $throughKey ?: $this->getForeignKey((new $through)->getName());
+
+ return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey);
+ }
+
+ /**
+ * BELONGS TO MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string $table 中间表名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型关联键
+ * @return BelongsToMany
+ */
+ public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+ $name = Loader::parseName(basename(str_replace('\\', '/', $model)));
+ $table = $table ?: Loader::parseName($this->name) . '_' . $name;
+ $foreignKey = $foreignKey ?: $name . '_id';
+ $localKey = $localKey ?: $this->getForeignKey($this->name);
+
+ return new BelongsToMany($this, $model, $table, $foreignKey, $localKey);
+ }
+
+ /**
+ * MORPH One 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
+ * @return MorphOne
+ */
+ public function morphOne($model, $morph = null, $type = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+
+ if (is_null($morph)) {
+ $trace = debug_backtrace(false, 2);
+ $morph = Loader::parseName($trace[1]['function']);
+ }
+
+ if (is_array($morph)) {
+ list($morphType, $foreignKey) = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ $type = $type ?: get_class($this);
+
+ return new MorphOne($this, $model, $foreignKey, $morphType, $type);
+ }
+
+ /**
+ * MORPH MANY 关联定义
+ * @access public
+ * @param string $model 模型名
+ * @param string|array $morph 多态字段信息
+ * @param string $type 多态类型
+ * @return MorphMany
+ */
+ public function morphMany($model, $morph = null, $type = '')
+ {
+ // 记录当前关联信息
+ $model = $this->parseModel($model);
+
+ if (is_null($morph)) {
+ $trace = debug_backtrace(false, 2);
+ $morph = Loader::parseName($trace[1]['function']);
+ }
+
+ $type = $type ?: get_class($this);
+
+ if (is_array($morph)) {
+ list($morphType, $foreignKey) = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ return new MorphMany($this, $model, $foreignKey, $morphType, $type);
+ }
+
+ /**
+ * MORPH TO 关联定义
+ * @access public
+ * @param string|array $morph 多态字段信息
+ * @param array $alias 多态别名定义
+ * @return MorphTo
+ */
+ public function morphTo($morph = null, $alias = [])
+ {
+ $trace = debug_backtrace(false, 2);
+ $relation = Loader::parseName($trace[1]['function']);
+
+ if (is_null($morph)) {
+ $morph = $relation;
+ }
+
+ // 记录当前关联信息
+ if (is_array($morph)) {
+ list($morphType, $foreignKey) = $morph;
+ } else {
+ $morphType = $morph . '_type';
+ $foreignKey = $morph . '_id';
+ }
+
+ return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
+ * @return string
+ */
+ protected function parseModel($model)
+ {
+ if (false === strpos($model, '\\')) {
+ $path = explode('\\', static::class);
+ array_pop($path);
+ array_push($path, Loader::parseName($model, 1));
+ $model = implode('\\', $path);
+ }
+
+ return $model;
+ }
+
+ /**
+ * 获取模型的默认外键名
+ * @access protected
+ * @param string $name 模型名
+ * @return string
+ */
+ protected function getForeignKey($name)
+ {
+ if (strpos($name, '\\')) {
+ $name = basename(str_replace('\\', '/', $name));
+ }
+
+ return Loader::parseName($name) . '_id';
+ }
+
+ /**
+ * 检查属性是否为关联属性 如果是则返回关联方法名
+ * @access protected
+ * @param string $attr 关联属性名
+ * @return string|false
+ */
+ protected function isRelationAttr($attr)
+ {
+ $relation = Loader::parseName($attr, 1, false);
+
+ if (method_exists($this, $relation)) {
+ return $relation;
+ }
+
+ return false;
+ }
+
+ /**
+ * 智能获取关联模型数据
+ * @access protected
+ * @param Relation $modelRelation 模型关联对象
+ * @return mixed
+ */
+ protected function getRelationData(Relation $modelRelation)
+ {
+ if ($this->parent && !$modelRelation->isSelfRelation() && get_class($this->parent) == get_class($modelRelation->getModel())) {
+ $value = $this->parent;
+ } else {
+ // 获取关联数据
+ $value = $modelRelation->getRelation();
+ }
+
+ return $value;
+ }
+
+ /**
+ * 关联数据自动写入检查
+ * @access protected
+ * @return void
+ */
+ protected function checkAutoRelationWrite()
+ {
+ foreach ($this->together as $key => $name) {
+ if (is_array($name)) {
+ if (key($name) === 0) {
+ $this->relationWrite[$key] = [];
+ // 绑定关联属性
+ foreach ((array) $name as $val) {
+ if (isset($this->data[$val])) {
+ $this->relationWrite[$key][$val] = $this->data[$val];
+ }
+ }
+ } else {
+ // 直接传入关联数据
+ $this->relationWrite[$key] = $name;
+ }
+ } elseif (isset($this->relation[$name])) {
+ $this->relationWrite[$name] = $this->relation[$name];
+ } elseif (isset($this->data[$name])) {
+ $this->relationWrite[$name] = $this->data[$name];
+ unset($this->data[$name]);
+ }
+ }
+ }
+
+ /**
+ * 自动关联数据更新(针对一对一关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationUpdate()
+ {
+ foreach ($this->relationWrite as $name => $val) {
+ if ($val instanceof Model) {
+ $val->isUpdate()->save();
+ } else {
+ $model = $this->getRelation($name);
+ if ($model instanceof Model) {
+ $model->isUpdate()->save($val);
+ }
+ }
+ }
+ }
+
+ /**
+ * 自动关联数据写入(针对一对一关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationInsert()
+ {
+ foreach ($this->relationWrite as $name => $val) {
+ $method = Loader::parseName($name, 1, false);
+ $this->$method()->save($val);
+ }
+ }
+
+ /**
+ * 自动关联数据删除(支持一对一及一对多关联)
+ * @access protected
+ * @return void
+ */
+ protected function autoRelationDelete()
+ {
+ foreach ($this->relationWrite as $key => $name) {
+ $name = is_numeric($key) ? $name : $key;
+ $result = $this->getRelation($name);
+
+ if ($result instanceof Model) {
+ $result->delete();
+ } elseif ($result instanceof Collection) {
+ foreach ($result as $model) {
+ $model->delete();
+ }
+ }
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/concern/SoftDelete.php b/thinkphp/library/think/model/concern/SoftDelete.php
new file mode 100644
index 00000000..7ba03243
--- /dev/null
+++ b/thinkphp/library/think/model/concern/SoftDelete.php
@@ -0,0 +1,240 @@
+getDeleteTimeField();
+
+ if ($field && !empty($this->getOrigin($field))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 查询软删除数据
+ * @access public
+ * @return Query
+ */
+ public static function withTrashed()
+ {
+ $model = new static();
+
+ return $model->withTrashedData(true)->db(false);
+ }
+
+ /**
+ * 是否包含软删除数据
+ * @access protected
+ * @param bool $withTrashed 是否包含软删除数据
+ * @return $this
+ */
+ protected function withTrashedData($withTrashed)
+ {
+ $this->withTrashed = $withTrashed;
+ return $this;
+ }
+
+ /**
+ * 只查询软删除数据
+ * @access public
+ * @return Query
+ */
+ public static function onlyTrashed()
+ {
+ $model = new static();
+ $field = $model->getDeleteTimeField(true);
+
+ if ($field) {
+ return $model
+ ->db(false)
+ ->useSoftDelete($field, $model->getWithTrashedExp());
+ }
+
+ return $model->db(false);
+ }
+
+ /**
+ * 获取软删除数据的查询条件
+ * @access protected
+ * @return array
+ */
+ protected function getWithTrashedExp()
+ {
+ return is_null($this->defaultSoftDelete) ?
+ ['notnull', ''] : ['<>', $this->defaultSoftDelete];
+ }
+
+ /**
+ * 删除当前的记录
+ * @access public
+ * @return bool
+ */
+ public function delete($force = false)
+ {
+ if (!$this->isExists() || false === $this->trigger('before_delete', $this)) {
+ return false;
+ }
+
+ $name = $this->getDeleteTimeField();
+
+ if ($name && !$force) {
+ // 软删除
+ $this->data($name, $this->autoWriteTimestamp($name));
+
+ $result = $this->isUpdate()->withEvent(false)->save();
+
+ $this->withEvent(true);
+ } else {
+ // 读取更新条件
+ $where = $this->getWhere();
+
+ // 删除当前模型数据
+ $result = $this->db(false)
+ ->where($where)
+ ->removeOption('soft_delete')
+ ->delete();
+ }
+
+ // 关联删除
+ if (!empty($this->relationWrite)) {
+ $this->autoRelationDelete();
+ }
+
+ $this->trigger('after_delete', $this);
+
+ $this->exists(false);
+
+ return true;
+ }
+
+ /**
+ * 删除记录
+ * @access public
+ * @param mixed $data 主键列表 支持闭包查询条件
+ * @param bool $force 是否强制删除
+ * @return bool
+ */
+ public static function destroy($data, $force = false)
+ {
+ // 包含软删除数据
+ $query = self::withTrashed();
+
+ if (is_array($data) && key($data) !== 0) {
+ $query->where($data);
+ $data = null;
+ } elseif ($data instanceof \Closure) {
+ call_user_func_array($data, [ & $query]);
+ $data = null;
+ } elseif (is_null($data)) {
+ return false;
+ }
+
+ $resultSet = $query->select($data);
+
+ if ($resultSet) {
+ foreach ($resultSet as $data) {
+ $data->force($force)->delete();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 恢复被软删除的记录
+ * @access public
+ * @param array $where 更新条件
+ * @return bool
+ */
+ public function restore($where = [])
+ {
+ $name = $this->getDeleteTimeField();
+
+ if ($name) {
+ if (false === $this->trigger('before_restore')) {
+ return false;
+ }
+
+ if (empty($where)) {
+ $pk = $this->getPk();
+
+ $where[] = [$pk, '=', $this->getData($pk)];
+ }
+
+ // 恢复删除
+ $this->db(false)
+ ->where($where)
+ ->useSoftDelete($name, $this->getWithTrashedExp())
+ ->update([$name => $this->defaultSoftDelete]);
+
+ $this->trigger('after_restore');
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取软删除字段
+ * @access protected
+ * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名
+ * @return string|false
+ */
+ protected function getDeleteTimeField($read = false)
+ {
+ $field = property_exists($this, 'deleteTime') && isset($this->deleteTime) ? $this->deleteTime : 'delete_time';
+
+ if (false === $field) {
+ return false;
+ }
+
+ if (!strpos($field, '.')) {
+ $field = '__TABLE__.' . $field;
+ }
+
+ if (!$read && strpos($field, '.')) {
+ $array = explode('.', $field);
+ $field = array_pop($array);
+ }
+
+ return $field;
+ }
+
+ /**
+ * 查询的时候默认排除软删除数据
+ * @access protected
+ * @param Query $query
+ * @return void
+ */
+ protected function withNoTrashed($query)
+ {
+ $field = $this->getDeleteTimeField(true);
+
+ if ($field) {
+ $query->useSoftDelete($field, $this->defaultSoftDelete);
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/concern/TimeStamp.php b/thinkphp/library/think/model/concern/TimeStamp.php
new file mode 100644
index 00000000..923b7d45
--- /dev/null
+++ b/thinkphp/library/think/model/concern/TimeStamp.php
@@ -0,0 +1,79 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\concern;
+
+/**
+ * 自动时间戳
+ */
+trait TimeStamp
+{
+ /**
+ * 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
+ * @var bool|string
+ */
+ protected $autoWriteTimestamp;
+
+ /**
+ * 创建时间字段 false表示关闭
+ * @var false|string
+ */
+ protected $createTime = 'create_time';
+
+ /**
+ * 更新时间字段 false表示关闭
+ * @var false|string
+ */
+ protected $updateTime = 'update_time';
+
+ /**
+ * 时间字段显示格式
+ * @var string
+ */
+ protected $dateFormat;
+
+ /**
+ * 时间日期字段格式化处理
+ * @access protected
+ * @param mixed $time 时间日期表达式
+ * @param mixed $format 日期格式
+ * @param bool $timestamp 是否进行时间戳转换
+ * @return mixed
+ */
+ protected function formatDateTime($time, $format, $timestamp = false)
+ {
+ if (false !== strpos($format, '\\')) {
+ $time = new $format($time);
+ } elseif (!$timestamp && false !== $format) {
+ $time = date($format, $time);
+ }
+
+ return $time;
+ }
+
+ /**
+ * 检查时间字段写入
+ * @access protected
+ * @return void
+ */
+ protected function checkTimeStampWrite()
+ {
+ // 自动写入创建时间和更新时间
+ if ($this->autoWriteTimestamp) {
+ if ($this->createTime && !isset($this->data[$this->createTime])) {
+ $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime);
+ }
+ if ($this->updateTime && !isset($this->data[$this->updateTime])) {
+ $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
+ }
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/relation/BelongsTo.php b/thinkphp/library/think/model/relation/BelongsTo.php
new file mode 100644
index 00000000..8ffa9ea1
--- /dev/null
+++ b/thinkphp/library/think/model/relation/BelongsTo.php
@@ -0,0 +1,251 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\Loader;
+use think\Model;
+
+class BelongsTo extends OneToOne
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 关联主键
+ * @param string $relation 关联名
+ */
+ public function __construct(Model $parent, $model, $foreignKey, $localKey, $relation = null)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+ $this->joinType = 'INNER';
+ $this->query = (new $model)->db();
+ $this->relation = $relation;
+
+ if (get_class($parent) == $model) {
+ $this->selfRelation = true;
+ }
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $foreignKey = $this->foreignKey;
+
+ $relationModel = $this->query
+ ->removeWhereField($this->localKey)
+ ->where($this->localKey, $this->parent->$foreignKey)
+ ->relation($subRelation)
+ ->find();
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ $table = $this->query->getTable();
+ $model = basename(str_replace('\\', '/', get_class($this->parent)));
+ $relation = basename(str_replace('\\', '/', $this->model));
+
+ if (is_array($where)) {
+ $this->getQueryWhere($where, $relation);
+ }
+
+ $fields = $this->getRelationQueryFields($fields, $model);
+
+ return $this->parent->db()
+ ->alias($model)
+ ->field($fields)
+ ->join([$table => $relation], $model . '.' . $this->foreignKey . '=' . $relation . '.' . $this->localKey, $this->joinType)
+ ->where($where);
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $range = [];
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$foreignKey)) {
+ $range[] = $result->$foreignKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $this->query->removeWhereField($localKey);
+
+ $data = $this->eagerlyWhere([
+ [$localKey, 'in', $range],
+ ], $localKey, $relation, $subRelation, $closure);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ // 关联模型
+ if (!isset($data[$result->$foreignKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$foreignKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($relationModel, $result);
+ } else {
+ // 设置关联属性
+ $result->setRelation($attr, $relationModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ protected function eagerlyOne(&$result, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $this->query->removeWhereField($localKey);
+
+ $data = $this->eagerlyWhere([
+ [$localKey, '=', $result->$foreignKey],
+ ], $localKey, $relation, $subRelation, $closure);
+
+ // 关联模型
+ if (!isset($data[$result->$foreignKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$foreignKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($relationModel, $result);
+ } else {
+ // 设置关联属性
+ $result->setRelation(Loader::parseName($relation), $relationModel);
+ }
+ }
+
+ /**
+ * 添加关联数据
+ * @access public
+ * @param Model $model 关联模型对象
+ * @return Model
+ */
+ public function associate($model)
+ {
+ $foreignKey = $this->foreignKey;
+ $pk = $model->getPk();
+
+ $this->parent->setAttr($foreignKey, $model->$pk);
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, $model);
+ }
+
+ /**
+ * 注销关联数据
+ * @access public
+ * @return Model
+ */
+ public function dissociate()
+ {
+ $foreignKey = $this->foreignKey;
+
+ $this->parent->setAttr($foreignKey, null);
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, null);
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery)) {
+ if (isset($this->parent->{$this->foreignKey})) {
+ // 关联查询带入关联条件
+ $this->query->where($this->localKey, '=', $this->parent->{$this->foreignKey});
+ }
+
+ $this->baseQuery = true;
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/relation/BelongsToMany.php b/thinkphp/library/think/model/relation/BelongsToMany.php
new file mode 100644
index 00000000..02e66669
--- /dev/null
+++ b/thinkphp/library/think/model/relation/BelongsToMany.php
@@ -0,0 +1,648 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\Collection;
+use think\db\Query;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Pivot;
+use think\model\Relation;
+
+class BelongsToMany extends Relation
+{
+ // 中间表表名
+ protected $middle;
+ // 中间表模型名称
+ protected $pivotName;
+ // 中间表模型对象
+ protected $pivot;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $table 中间表名
+ * @param string $foreignKey 关联模型外键
+ * @param string $localKey 当前模型关联键
+ */
+ public function __construct(Model $parent, $model, $table, $foreignKey, $localKey)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+
+ if (false !== strpos($table, '\\')) {
+ $this->pivotName = $table;
+ $this->middle = basename(str_replace('\\', '/', $table));
+ } else {
+ $this->middle = $table;
+ }
+
+ $this->query = (new $model)->db();
+ $this->pivot = $this->newPivot();
+ }
+
+ /**
+ * 设置中间表模型
+ * @access public
+ * @param $pivot
+ * @return $this
+ */
+ public function pivot($pivot)
+ {
+ $this->pivotName = $pivot;
+ return $this;
+ }
+
+ /**
+ * 获取中间表更新条件
+ * @param $data
+ * @return array
+ */
+ protected function getUpdateWhere($data)
+ {
+ return [
+ $this->localKey => $data[$this->localKey],
+ $this->foreignKey => $data[$this->foreignKey],
+ ];
+ }
+
+ /**
+ * 实例化中间表模型
+ * @access public
+ * @param array $data
+ * @param bool $isUpdate
+ * @return Pivot
+ * @throws Exception
+ */
+ protected function newPivot($data = [], $isUpdate = false)
+ {
+ $class = $this->pivotName ?: '\\think\\model\\Pivot';
+ $pivot = new $class($data, $this->parent, $this->middle);
+
+ if ($pivot instanceof Pivot) {
+ return $isUpdate ? $pivot->isUpdate(true, $this->getUpdateWhere($data)) : $pivot;
+ }
+
+ throw new Exception('pivot model must extends: \think\model\Pivot');
+ }
+
+ /**
+ * 合成中间表模型
+ * @access protected
+ * @param array|Collection|Paginator $models
+ */
+ protected function hydratePivot($models)
+ {
+ foreach ($models as $model) {
+ $pivot = [];
+
+ foreach ($model->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ list($name, $attr) = explode('__', $key, 2);
+
+ if ('pivot' == $name) {
+ $pivot[$attr] = $val;
+ unset($model->$key);
+ }
+ }
+ }
+
+ $model->setRelation('pivot', $this->newPivot($pivot, true));
+ }
+ }
+
+ /**
+ * 创建关联查询Query对象
+ * @access protected
+ * @return Query
+ */
+ protected function buildQuery()
+ {
+ $foreignKey = $this->foreignKey;
+ $localKey = $this->localKey;
+
+ // 关联查询
+ $pk = $this->parent->getPk();
+
+ $condition[] = ['pivot.' . $localKey, '=', $this->parent->$pk];
+
+ return $this->belongsToManyQuery($foreignKey, $localKey, $condition);
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return Collection
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $result = $this->buildQuery()->relation($subRelation)->select();
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载select方法
+ * @access public
+ * @param mixed $data
+ * @return Collection
+ */
+ public function select($data = null)
+ {
+ $result = $this->buildQuery()->select($data);
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载paginate方法
+ * @access public
+ * @param null $listRows
+ * @param bool $simple
+ * @param array $config
+ * @return Paginator
+ */
+ public function paginate($listRows = null, $simple = false, $config = [])
+ {
+ $result = $this->buildQuery()->paginate($listRows, $simple, $config);
+ $this->hydratePivot($result);
+
+ return $result;
+ }
+
+ /**
+ * 重载find方法
+ * @access public
+ * @param mixed $data
+ * @return Model
+ */
+ public function find($data = null)
+ {
+ $result = $this->buildQuery()->find($data);
+ if ($result) {
+ $this->hydratePivot([$result]);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 查找多条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|\Closure $data
+ * @return Collection
+ */
+ public function selectOrFail($data = null)
+ {
+ return $this->failException(true)->select($data);
+ }
+
+ /**
+ * 查找单条记录 如果不存在则抛出异常
+ * @access public
+ * @param array|string|Query|\Closure $data
+ * @return Model
+ */
+ public function findOrFail($data = null)
+ {
+ return $this->failException(true)->find($data);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ * @throws Exception
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 设置中间表的查询条件
+ * @access public
+ * @param string $field
+ * @param string $op
+ * @param mixed $condition
+ * @return $this
+ */
+ public function wherePivot($field, $op = null, $condition = null)
+ {
+ $this->query->where('pivot.' . $field, $op, $condition);
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $pk = $resultSet[0]->getPk();
+ $range = [];
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$pk)) {
+ $range[] = $result->$pk;
+ }
+ }
+
+ if (!empty($range)) {
+ // 查询关联数据
+ $data = $this->eagerlyManyToMany([
+ ['pivot.' . $localKey, 'in', $range],
+ ], $relation, $subRelation);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ if (!isset($data[$result->$pk])) {
+ $data[$result->$pk] = [];
+ }
+
+ $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk]));
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(单个数据)
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ $pk = $result->getPk();
+
+ if (isset($result->$pk)) {
+ $pk = $result->$pk;
+ // 查询管理数据
+ $data = $this->eagerlyManyToMany([
+ ['pivot.' . $this->localKey, '=', $pk],
+ ], $relation, $subRelation);
+
+ // 关联数据封装
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk]));
+ }
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {
+ $pk = $result->getPk();
+ $count = 0;
+
+ if (isset($result->$pk)) {
+ $pk = $result->$pk;
+ $count = $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
+ ['pivot.' . $this->localKey, '=', $pk],
+ ])->$aggregate($field);
+ }
+
+ return $count;
+ }
+
+ /**
+ * 获取关联统计子查询
+ * @access public
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return string
+ */
+ public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*')
+ {
+ return $this->belongsToManyQuery($this->foreignKey, $this->localKey, [
+ [
+ 'pivot.' . $this->localKey, 'exp', $this->query->raw('=' . $this->parent->getTable() . '.' . $this->parent->getPk()),
+ ],
+ ])->fetchSql()->$aggregate($field);
+ }
+
+ /**
+ * 多对多 关联模型预查询
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @return array
+ */
+ protected function eagerlyManyToMany($where, $relation, $subRelation = '')
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $list = $this->belongsToManyQuery($this->foreignKey, $this->localKey, $where)
+ ->with($subRelation)
+ ->select();
+
+ // 组装模型数据
+ $data = [];
+ foreach ($list as $set) {
+ $pivot = [];
+ foreach ($set->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ list($name, $attr) = explode('__', $key, 2);
+ if ('pivot' == $name) {
+ $pivot[$attr] = $val;
+ unset($set->$key);
+ }
+ }
+ }
+
+ $set->setRelation('pivot', $this->newPivot($pivot, true));
+
+ $data[$pivot[$this->localKey]][] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * BELONGS TO MANY 关联查询
+ * @access protected
+ * @param string $foreignKey 关联模型关联键
+ * @param string $localKey 当前模型关联键
+ * @param array $condition 关联查询条件
+ * @return Query
+ */
+ protected function belongsToManyQuery($foreignKey, $localKey, $condition = [])
+ {
+ // 关联查询封装
+ $tableName = $this->query->getTable();
+ $table = $this->pivot->getTable();
+ $fields = $this->getQueryFields($tableName);
+
+ $query = $this->query
+ ->field($fields)
+ ->field(true, false, $table, 'pivot', 'pivot__');
+
+ if (empty($this->baseQuery)) {
+ $relationFk = $this->query->getPk();
+ $query->join([$table => 'pivot'], 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk)
+ ->where($condition);
+ }
+
+ return $query;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param array $pivot 中间表额外数据
+ * @return array|Pivot
+ */
+ public function save($data, array $pivot = [])
+ {
+ // 保存关联表/中间表数据
+ return $this->attach($data, $pivot);
+ }
+
+ /**
+ * 批量保存当前关联数据对象
+ * @access public
+ * @param array $dataSet 数据集
+ * @param array $pivot 中间表额外数据
+ * @param bool $samePivot 额外数据是否相同
+ * @return array|false
+ */
+ public function saveAll(array $dataSet, array $pivot = [], $samePivot = false)
+ {
+ $result = [];
+
+ foreach ($dataSet as $key => $data) {
+ if (!$samePivot) {
+ $pivotData = isset($pivot[$key]) ? $pivot[$key] : [];
+ } else {
+ $pivotData = $pivot;
+ }
+
+ $result[] = $this->attach($data, $pivotData);
+ }
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * 附加关联的一个中间表数据
+ * @access public
+ * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键
+ * @param array $pivot 中间表额外数据
+ * @return array|Pivot
+ * @throws Exception
+ */
+ public function attach($data, $pivot = [])
+ {
+ if (is_array($data)) {
+ if (key($data) === 0) {
+ $id = $data;
+ } else {
+ // 保存关联表数据
+ $model = new $this->model;
+ $model->save($data);
+ $id = $model->getLastInsID();
+ }
+ } elseif (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } elseif ($data instanceof Model) {
+ // 根据关联表主键直接写入中间表
+ $relationFk = $data->getPk();
+ $id = $data->$relationFk;
+ }
+
+ if ($id) {
+ // 保存中间表数据
+ $pk = $this->parent->getPk();
+ $pivot[$this->localKey] = $this->parent->$pk;
+ $ids = (array) $id;
+
+ foreach ($ids as $id) {
+ $pivot[$this->foreignKey] = $id;
+ $this->pivot->insert($pivot, true);
+ $result[] = $this->newPivot($pivot, true);
+ }
+
+ if (count($result) == 1) {
+ // 返回中间表模型对象
+ $result = $result[0];
+ }
+
+ return $result;
+ } else {
+ throw new Exception('miss relation data');
+ }
+ }
+
+ /**
+ * 解除关联的一个中间表数据
+ * @access public
+ * @param integer|array $data 数据 可以使用关联对象的主键
+ * @param bool $relationDel 是否同时删除关联表数据
+ * @return integer
+ */
+ public function detach($data = null, $relationDel = false)
+ {
+ if (is_array($data)) {
+ $id = $data;
+ } elseif (is_numeric($data) || is_string($data)) {
+ // 根据关联表主键直接写入中间表
+ $id = $data;
+ } elseif ($data instanceof Model) {
+ // 根据关联表主键直接写入中间表
+ $relationFk = $data->getPk();
+ $id = $data->$relationFk;
+ }
+
+ // 删除中间表数据
+ $pk = $this->parent->getPk();
+ $pivot[] = [$this->localKey, '=', $this->parent->$pk];
+
+ if (isset($id)) {
+ $pivot[] = [$this->foreignKey, is_array($id) ? 'in' : '=', $id];
+ }
+
+ $result = $this->pivot->where($pivot)->delete();
+
+ // 删除关联表数据
+ if (isset($id) && $relationDel) {
+ $model = $this->model;
+ $model::destroy($id);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 数据同步
+ * @access public
+ * @param array $ids
+ * @param bool $detaching
+ * @return array
+ */
+ public function sync($ids, $detaching = true)
+ {
+ $changes = [
+ 'attached' => [],
+ 'detached' => [],
+ 'updated' => [],
+ ];
+
+ $pk = $this->parent->getPk();
+
+ $current = $this->pivot
+ ->where($this->localKey, $this->parent->$pk)
+ ->column($this->foreignKey);
+
+ $records = [];
+
+ foreach ($ids as $key => $value) {
+ if (!is_array($value)) {
+ $records[$value] = [];
+ } else {
+ $records[$key] = $value;
+ }
+ }
+
+ $detach = array_diff($current, array_keys($records));
+
+ if ($detaching && count($detach) > 0) {
+ $this->detach($detach);
+ $changes['detached'] = $detach;
+ }
+
+ foreach ($records as $id => $attributes) {
+ if (!in_array($id, $current)) {
+ $this->attach($id, $attributes);
+ $changes['attached'][] = $id;
+ } elseif (count($attributes) > 0 && $this->attach($id, $attributes)) {
+ $changes['updated'][] = $id;
+ }
+ }
+
+ return $changes;
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery) && $this->parent->getData()) {
+ $pk = $this->parent->getPk();
+ $table = $this->pivot->getTable();
+
+ $this->query
+ ->join([$table => 'pivot'], 'pivot.' . $this->foreignKey . '=' . $this->query->getTable() . '.' . $this->query->getPk())
+ ->where('pivot.' . $this->localKey, $this->parent->$pk);
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/HasMany.php b/thinkphp/library/think/model/relation/HasMany.php
new file mode 100644
index 00000000..2038a960
--- /dev/null
+++ b/thinkphp/library/think/model/relation/HasMany.php
@@ -0,0 +1,331 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+class HasMany extends Relation
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型主键
+ */
+ public function __construct(Model $parent, $model, $foreignKey, $localKey)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+ $this->query = (new $model)->db();
+
+ if (get_class($parent) == $model) {
+ $this->selfRelation = true;
+ }
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return \think\Collection
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $list = $this->query
+ ->where($this->foreignKey, $this->parent->{$this->localKey})
+ ->relation($subRelation)
+ ->select();
+
+ $parent = clone $this->parent;
+
+ foreach ($list as &$model) {
+ $model->setParent($parent);
+ }
+
+ return $list;
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$localKey)) {
+ $range[] = $result->$localKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $where = [
+ [$this->foreignKey, 'in', $range],
+ ];
+ $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ $pk = $result->$localKey;
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ foreach ($data[$pk] as &$relationModel) {
+ $relationModel->setParent(clone $result);
+ }
+
+ $result->setRelation($attr, $this->resultSetBuild($data[$pk]));
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+
+ if (isset($result->$localKey)) {
+ $pk = $result->$localKey;
+ $where = [
+ [$this->foreignKey, '=', $pk],
+ ];
+ $data = $this->eagerlyOneToMany($where, $relation, $subRelation, $closure);
+
+ // 关联数据封装
+ if (!isset($data[$pk])) {
+ $data[$pk] = [];
+ }
+
+ foreach ($data[$pk] as &$relationModel) {
+ $relationModel->setParent(clone $result);
+ }
+
+ $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$pk]));
+ }
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {
+ $localKey = $this->localKey;
+ $count = 0;
+
+ if (isset($result->$localKey)) {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $count = $this->query->where($this->foreignKey, '=', $result->$localKey)->$aggregate($field);
+ }
+
+ return $count;
+ }
+
+ /**
+ * 创建关联统计子查询
+ * @access public
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return string
+ */
+ public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*')
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ return $this->query
+ ->whereExp($this->foreignKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
+ ->fetchSql()
+ ->$aggregate($field);
+ }
+
+ /**
+ * 一对多 关联模型预查询
+ * @access public
+ * @param array $where 关联预查询条件
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @param \Closure $closure
+ * @return array
+ */
+ protected function eagerlyOneToMany($where, $relation, $subRelation = '', $closure = null)
+ {
+ $foreignKey = $this->foreignKey;
+
+ $this->query->removeWhereField($this->foreignKey);
+
+ // 预载入关联查询 支持嵌套预载入
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $list = $this->query->where($where)->with($subRelation)->select();
+
+ // 组装模型数据
+ $data = [];
+
+ foreach ($list as $set) {
+ $data[$set->$foreignKey][] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @param boolean $replace 是否自动识别更新和写入
+ * @return Model|false
+ */
+ public function save($data, $replace = true)
+ {
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
+
+ // 保存关联表数据
+ $data[$this->foreignKey] = $this->parent->{$this->localKey};
+
+ $model = new $this->model;
+
+ return $model->replace($replace)->save($data) ? $model : false;
+ }
+
+ /**
+ * 批量保存当前关联数据对象
+ * @access public
+ * @param array $dataSet 数据集
+ * @param boolean $replace 是否自动识别更新和写入
+ * @return array|false
+ */
+ public function saveAll(array $dataSet, $replace = true)
+ {
+ $result = [];
+
+ foreach ($dataSet as $key => $data) {
+ $result[] = $this->save($data, $replace);
+ }
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ $table = $this->query->getTable();
+ $model = basename(str_replace('\\', '/', get_class($this->parent)));
+ $relation = basename(str_replace('\\', '/', $this->model));
+
+ return $this->parent->db()
+ ->alias($model)
+ ->field($model . '.*')
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $joinType)
+ ->group($relation . '.' . $this->foreignKey)
+ ->having('count(' . $id . ')' . $operator . $count);
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ $table = $this->query->getTable();
+ $model = basename(str_replace('\\', '/', get_class($this->parent)));
+ $relation = basename(str_replace('\\', '/', $this->model));
+
+ if (is_array($where)) {
+ $this->getQueryWhere($where, $relation);
+ }
+
+ $fields = $this->getRelationQueryFields($fields, $model);
+
+ return $this->parent->db()
+ ->alias($model)
+ ->group($model . '.' . $this->localKey)
+ ->field($fields)
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey)
+ ->where($where);
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery)) {
+ if (isset($this->parent->{$this->localKey})) {
+ // 关联查询带入关联条件
+ $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
+ }
+
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/HasManyThrough.php b/thinkphp/library/think/model/relation/HasManyThrough.php
new file mode 100644
index 00000000..411f3fcc
--- /dev/null
+++ b/thinkphp/library/think/model/relation/HasManyThrough.php
@@ -0,0 +1,155 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+class HasManyThrough extends Relation
+{
+ // 中间关联表外键
+ protected $throughKey;
+ // 中间表模型
+ protected $through;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $through 中间模型名
+ * @param string $foreignKey 关联外键
+ * @param string $throughKey 关联外键
+ * @param string $localKey 当前主键
+ */
+ public function __construct(Model $parent, $model, $through, $foreignKey, $throughKey, $localKey)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->through = $through;
+ $this->foreignKey = $foreignKey;
+ $this->throughKey = $throughKey;
+ $this->localKey = $localKey;
+ $this->query = (new $model)->db();
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return \think\Collection
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $this->baseQuery();
+
+ return $this->query->relation($subRelation)->select();
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {}
+
+ /**
+ * 预载入关联查询 返回模型对象
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {}
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {}
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery) && $this->parent->getData()) {
+ $through = $this->through;
+ $alias = Loader::parseName(basename(str_replace('\\', '/', $this->model)));
+ $throughTable = $through::getTable();
+ $pk = (new $through)->getPk();
+ $throughKey = $this->throughKey;
+ $modelTable = $this->parent->getTable();
+ $fields = $this->getQueryFields($alias);
+
+ $this->query
+ ->field($fields)
+ ->alias($alias)
+ ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey)
+ ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey)
+ ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey});
+
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/HasOne.php b/thinkphp/library/think/model/relation/HasOne.php
new file mode 100644
index 00000000..3ce5fea0
--- /dev/null
+++ b/thinkphp/library/think/model/relation/HasOne.php
@@ -0,0 +1,230 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Loader;
+use think\Model;
+
+class HasOne extends OneToOne
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $foreignKey 关联外键
+ * @param string $localKey 当前模型主键
+ */
+ public function __construct(Model $parent, $model, $foreignKey, $localKey)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->foreignKey = $foreignKey;
+ $this->localKey = $localKey;
+ $this->joinType = 'INNER';
+ $this->query = (new $model)->db();
+
+ if (get_class($parent) == $model) {
+ $this->selfRelation = true;
+ }
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ $localKey = $this->localKey;
+
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ // 判断关联类型执行查询
+ $relationModel = $this->query
+ ->removeWhereField($this->foreignKey)
+ ->where($this->foreignKey, $this->parent->$localKey)
+ ->relation($subRelation)
+ ->find();
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ $table = $this->query->getTable();
+ $model = basename(str_replace('\\', '/', get_class($this->parent)));
+ $relation = basename(str_replace('\\', '/', $this->model));
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ return $this->parent->db()
+ ->alias($model)
+ ->whereExists(function ($query) use ($table, $model, $relation, $localKey, $foreignKey) {
+ $query->table([$table => $relation])
+ ->field($relation . '.' . $foreignKey)
+ ->whereExp($model . '.' . $localKey, '=' . $relation . '.' . $foreignKey);
+ });
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ $table = $this->query->getTable();
+ $model = basename(str_replace('\\', '/', get_class($this->parent)));
+ $relation = basename(str_replace('\\', '/', $this->model));
+
+ if (is_array($where)) {
+ $this->getQueryWhere($where, $relation);
+ }
+
+ $fields = $this->getRelationQueryFields($fields, $model);
+
+ return $this->parent->db()
+ ->alias($model)
+ ->field($fields)
+ ->join([$table => $relation], $model . '.' . $this->localKey . '=' . $relation . '.' . $this->foreignKey, $this->joinType)
+ ->where($where);
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access protected
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $range = [];
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (isset($result->$localKey)) {
+ $range[] = $result->$localKey;
+ }
+ }
+
+ if (!empty($range)) {
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$foreignKey, 'in', $range],
+ ], $foreignKey, $relation, $subRelation, $closure);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ // 关联模型
+ if (!isset($data[$result->$localKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($relationModel, $result, $this->bindAttr);
+ } else {
+ // 设置关联属性
+ $result->setRelation($attr, $relationModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @access protected
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ protected function eagerlyOne(&$result, $relation, $subRelation, $closure)
+ {
+ $localKey = $this->localKey;
+ $foreignKey = $this->foreignKey;
+
+ $this->query->removeWhereField($foreignKey);
+
+ $data = $this->eagerlyWhere([
+ [$foreignKey, '=', $result->$localKey],
+ ], $foreignKey, $relation, $subRelation, $closure);
+
+ // 关联模型
+ if (!isset($data[$result->$localKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$localKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ if (!empty($this->bindAttr)) {
+ // 绑定关联属性
+ $this->bindAttr($relationModel, $result, $this->bindAttr);
+ } else {
+ $result->setRelation(Loader::parseName($relation), $relationModel);
+ }
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery)) {
+ if (isset($this->parent->{$this->localKey})) {
+ // 关联查询带入关联条件
+ $this->query->where($this->foreignKey, '=', $this->parent->{$this->localKey});
+ }
+
+ $this->baseQuery = true;
+ }
+ }
+}
diff --git a/thinkphp/library/think/model/relation/MorphMany.php b/thinkphp/library/think/model/relation/MorphMany.php
new file mode 100644
index 00000000..3e913ccd
--- /dev/null
+++ b/thinkphp/library/think/model/relation/MorphMany.php
@@ -0,0 +1,322 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+class MorphMany extends Relation
+{
+ // 多态字段
+ protected $morphKey;
+ protected $morphType;
+ // 多态类型
+ protected $type;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $type 多态类型
+ */
+ public function __construct(Model $parent, $model, $morphKey, $morphType, $type)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->type = $type;
+ $this->morphKey = $morphKey;
+ $this->morphType = $morphType;
+ $this->query = (new $model)->db();
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return \think\Collection
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $this->baseQuery();
+
+ $list = $this->query->relation($subRelation)->select();
+ $parent = clone $this->parent;
+
+ foreach ($list as &$model) {
+ $model->setParent($parent);
+ }
+
+ return $list;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ throw new Exception('relation not support: has');
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $morphType = $this->morphType;
+ $morphKey = $this->morphKey;
+ $type = $this->type;
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ $pk = $result->getPk();
+ // 获取关联外键列表
+ if (isset($result->$pk)) {
+ $range[] = $result->$pk;
+ }
+ }
+
+ if (!empty($range)) {
+ $where = [
+ [$morphKey, 'in', $range],
+ [$morphType, '=', $type],
+ ];
+ $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ if (!isset($data[$result->$pk])) {
+ $data[$result->$pk] = [];
+ }
+
+ foreach ($data[$result->$pk] as &$relationModel) {
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ $result->setRelation($attr, $this->resultSetBuild($data[$result->$pk]));
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ $pk = $result->getPk();
+
+ if (isset($result->$pk)) {
+ $key = $result->$pk;
+ $where = [
+ [$this->morphKey, '=', $key],
+ [$this->morphType, '=', $this->type],
+ ];
+ $data = $this->eagerlyMorphToMany($where, $relation, $subRelation, $closure);
+
+ if (!isset($data[$key])) {
+ $data[$key] = [];
+ }
+
+ foreach ($data[$key] as &$relationModel) {
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ $result->setRelation(Loader::parseName($relation), $this->resultSetBuild($data[$key]));
+ }
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {
+ $pk = $result->getPk();
+ $count = 0;
+
+ if (isset($result->$pk)) {
+ if ($closure) {
+ $closur($this->query);
+ }
+
+ $count = $this->query
+ ->where([
+ [$this->morphKey, '=', $result->$pk],
+ [$this->morphType, '=', $this->type],
+ ])
+ ->$aggregate($field);
+ }
+
+ return $count;
+ }
+
+ /**
+ * 获取关联统计子查询
+ * @access public
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return string
+ */
+ public function getRelationCountQuery($closure, $aggregate = 'count', $field = '*')
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ return $this->query
+ ->whereExp($this->morphKey, '=' . $this->parent->getTable() . '.' . $this->parent->getPk())
+ ->where($this->morphType, '=', $this->type)
+ ->fetchSql()
+ ->$aggregate($field);
+ }
+
+ /**
+ * 多态一对多 关联模型预查询
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @param \Closure $closure 闭包
+ * @return array
+ */
+ protected function eagerlyMorphToMany($where, $relation, $subRelation = '', $closure = null)
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $this->query->removeOption('where');
+
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $list = $this->query->where($where)->with($subRelation)->select();
+ $morphKey = $this->morphKey;
+
+ // 组装模型数据
+ $data = [];
+ foreach ($list as $set) {
+ $data[$set->$morphKey][] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @return Model|false
+ */
+ public function save($data)
+ {
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
+
+ // 保存关联表数据
+ $pk = $this->parent->getPk();
+
+ $model = new $this->model;
+
+ $data[$this->morphKey] = $this->parent->$pk;
+ $data[$this->morphType] = $this->type;
+
+ return $model->save($data) ? $model : false;
+ }
+
+ /**
+ * 批量保存当前关联数据对象
+ * @access public
+ * @param array $dataSet 数据集
+ * @return array|false
+ */
+ public function saveAll(array $dataSet)
+ {
+ $result = [];
+
+ foreach ($dataSet as $key => $data) {
+ $result[] = $this->save($data);
+ }
+
+ return empty($result) ? false : $result;
+ }
+
+ /**
+ * 执行基础查询(仅执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery) && $this->parent->getData()) {
+ $pk = $this->parent->getPk();
+
+ $this->query->where([
+ [$this->morphKey, '=', $this->parent->$pk],
+ [$this->morphType, '=', $this->type],
+ ]);
+
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/MorphOne.php b/thinkphp/library/think/model/relation/MorphOne.php
new file mode 100644
index 00000000..ede680c6
--- /dev/null
+++ b/thinkphp/library/think/model/relation/MorphOne.php
@@ -0,0 +1,245 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+class MorphOne extends Relation
+{
+ // 多态字段
+ protected $morphKey;
+ protected $morphType;
+ // 多态类型
+ protected $type;
+
+ /**
+ * 构造函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $model 模型名
+ * @param string $morphKey 关联外键
+ * @param string $morphType 多态字段名
+ * @param string $type 多态类型
+ */
+ public function __construct(Model $parent, $model, $morphKey, $morphType, $type)
+ {
+ $this->parent = $parent;
+ $this->model = $model;
+ $this->type = $type;
+ $this->morphKey = $morphKey;
+ $this->morphType = $morphType;
+ $this->query = (new $model)->db();
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $this->baseQuery();
+
+ $relationModel = $this->query->relation($subRelation)->find();
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $morphType = $this->morphType;
+ $morphKey = $this->morphKey;
+ $type = $this->type;
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ $pk = $result->getPk();
+ // 获取关联外键列表
+ if (isset($result->$pk)) {
+ $range[] = $result->$pk;
+ }
+ }
+
+ if (!empty($range)) {
+ $data = $this->eagerlyMorphToOne([
+ [$morphKey, 'in', $range],
+ [$morphType, '=', $type],
+ ], $relation, $subRelation, $closure);
+
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ // 关联数据封装
+ foreach ($resultSet as $result) {
+ if (!isset($data[$result->$pk])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$pk];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ $result->setRelation($attr, $relationModel);
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ $pk = $result->getPk();
+
+ if (isset($result->$pk)) {
+ $pk = $result->$pk;
+ $data = $this->eagerlyMorphToOne([
+ [$this->morphKey, '=', $pk],
+ [$this->morphType, '=', $this->type],
+ ], $relation, $subRelation, $closure);
+
+ if (isset($data[$pk])) {
+ $relationModel = $data[$pk];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ } else {
+ $relationModel = null;
+ }
+
+ $result->setRelation(Loader::parseName($relation), $relationModel);
+ }
+ }
+
+ /**
+ * 多态一对一 关联模型预查询
+ * @access protected
+ * @param array $where 关联预查询条件
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @param \Closure $closure 闭包
+ * @return array
+ */
+ protected function eagerlyMorphToOne($where, $relation, $subRelation = '', $closure = null)
+ {
+ // 预载入关联查询 支持嵌套预载入
+ if ($closure) {
+ $closure($this->query);
+ }
+
+ $list = $this->query->where($where)->with($subRelation)->find();
+ $morphKey = $this->morphKey;
+
+ // 组装模型数据
+ $data = [];
+
+ foreach ($list as $set) {
+ $data[$set->$morphKey] = $set;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @return Model|false
+ */
+ public function save($data)
+ {
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
+ // 保存关联表数据
+ $pk = $this->parent->getPk();
+
+ $model = new $this->model;
+
+ $data[$this->morphKey] = $this->parent->$pk;
+ $data[$this->morphType] = $this->type;
+ return $model->save($data) ? $model : false;
+ }
+
+ /**
+ * 执行基础查询(进执行一次)
+ * @access protected
+ * @return void
+ */
+ protected function baseQuery()
+ {
+ if (empty($this->baseQuery) && $this->parent->getData()) {
+ $pk = $this->parent->getPk();
+
+ $this->query->where([
+ [$this->morphKey, '=', $this->parent->$pk],
+ [$this->morphType, '=', $this->type],
+ ]);
+ $this->baseQuery = true;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/MorphTo.php b/thinkphp/library/think/model/relation/MorphTo.php
new file mode 100644
index 00000000..bb7c4d0b
--- /dev/null
+++ b/thinkphp/library/think/model/relation/MorphTo.php
@@ -0,0 +1,306 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+class MorphTo extends Relation
+{
+ // 多态字段
+ protected $morphKey;
+ protected $morphType;
+ // 多态别名
+ protected $alias;
+ // 关联名
+ protected $relation;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Model $parent 上级模型对象
+ * @param string $morphType 多态字段名
+ * @param string $morphKey 外键名
+ * @param array $alias 多态别名定义
+ * @param string $relation 关联名
+ */
+ public function __construct(Model $parent, $morphType, $morphKey, $alias = [], $relation = null)
+ {
+ $this->parent = $parent;
+ $this->morphType = $morphType;
+ $this->morphKey = $morphKey;
+ $this->alias = $alias;
+ $this->relation = $relation;
+ }
+
+ /**
+ * 获取当前的关联模型类的实例
+ * @access public
+ * @return Model
+ */
+ public function getModel()
+ {
+ $morphType = $this->morphType;
+ $model = $this->parseModel($this->parent->$morphType);
+
+ return (new $model);
+ }
+
+ /**
+ * 延迟获取关联数据
+ * @access public
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包查询条件
+ * @return Model
+ */
+ public function getRelation($subRelation = '', $closure = null)
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+
+ // 多态模型
+ $model = $this->parseModel($this->parent->$morphType);
+
+ // 主键数据
+ $pk = $this->parent->$morphKey;
+
+ $relationModel = (new $model)->relation($subRelation)->find($pk);
+
+ if ($relationModel) {
+ $relationModel->setParent(clone $this->parent);
+ }
+
+ return $relationModel;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param string $operator 比较操作符
+ * @param integer $count 个数
+ * @param string $id 关联表的统计字段
+ * @param string $joinType JOIN类型
+ * @return Query
+ */
+ public function has($operator = '>=', $count = 1, $id = '*', $joinType = 'INNER')
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 根据关联条件查询当前模型
+ * @access public
+ * @param mixed $where 查询条件(数组或者闭包)
+ * @param mixed $fields 字段
+ * @return Query
+ */
+ public function hasWhere($where = [], $fields = null)
+ {
+ throw new Exception('relation not support: hasWhere');
+ }
+
+ /**
+ * 解析模型的完整命名空间
+ * @access protected
+ * @param string $model 模型名(或者完整类名)
+ * @return string
+ */
+ protected function parseModel($model)
+ {
+ if (isset($this->alias[$model])) {
+ $model = $this->alias[$model];
+ }
+
+ if (false === strpos($model, '\\')) {
+ $path = explode('\\', get_class($this->parent));
+ array_pop($path);
+ array_push($path, Loader::parseName($model, 1));
+ $model = implode('\\', $path);
+ }
+
+ return $model;
+ }
+
+ /**
+ * 设置多态别名
+ * @access public
+ * @param array $alias 别名定义
+ * @return $this
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+
+ return $this;
+ }
+
+ /**
+ * 移除关联查询参数
+ * @access public
+ * @return $this
+ */
+ public function removeOption()
+ {
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ * @throws Exception
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+ $range = [];
+
+ foreach ($resultSet as $result) {
+ // 获取关联外键列表
+ if (!empty($result->$morphKey)) {
+ $range[$result->$morphType][] = $result->$morphKey;
+ }
+ }
+
+ if (!empty($range)) {
+ // 关联属性名
+ $attr = Loader::parseName($relation);
+
+ foreach ($range as $key => $val) {
+ // 多态类型映射
+ $model = $this->parseModel($key);
+ $obj = new $model;
+ $pk = $obj->getPk();
+ $list = $obj->all($val, $subRelation);
+ $data = [];
+
+ foreach ($list as $k => $vo) {
+ $data[$vo->$pk] = $vo;
+ }
+
+ foreach ($resultSet as $result) {
+ if ($key == $result->$morphType) {
+ // 关联模型
+ if (!isset($data[$result->$morphKey])) {
+ $relationModel = null;
+ } else {
+ $relationModel = $data[$result->$morphKey];
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+ }
+
+ $result->setRelation($attr, $relationModel);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+ // 多态类型映射
+ $model = $this->parseModel($result->{$this->morphType});
+
+ $this->eagerlyMorphToOne($model, $relation, $result, $subRelation);
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {}
+
+ /**
+ * 多态MorphTo 关联模型预查询
+ * @access protected
+ * @param string $model 关联模型对象
+ * @param string $relation 关联名
+ * @param Model $result
+ * @param string $subRelation 子关联
+ * @return void
+ */
+ protected function eagerlyMorphToOne($model, $relation, &$result, $subRelation = '')
+ {
+ // 预载入关联查询 支持嵌套预载入
+ $pk = $this->parent->{$this->morphKey};
+ $data = (new $model)->with($subRelation)->find($pk);
+
+ if ($data) {
+ $data->setParent(clone $result);
+ $data->isUpdate(true);
+ }
+
+ $result->setRelation(Loader::parseName($relation), $data ?: null);
+ }
+
+ /**
+ * 添加关联数据
+ * @access public
+ * @param Model $model 关联模型对象
+ * @param string $type 多态类型
+ * @return Model
+ */
+ public function associate($model, $type = '')
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+ $pk = $model->getPk();
+
+ $this->parent->setAttr($morphKey, $model->$pk);
+ $this->parent->setAttr($morphType, $type ?: get_class($model));
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, $model);
+ }
+
+ /**
+ * 注销关联数据
+ * @access public
+ * @return Model
+ */
+ public function dissociate()
+ {
+ $morphKey = $this->morphKey;
+ $morphType = $this->morphType;
+
+ $this->parent->setAttr($morphKey, null);
+ $this->parent->setAttr($morphType, null);
+ $this->parent->save();
+
+ return $this->parent->setRelation($this->relation, null);
+ }
+
+}
diff --git a/thinkphp/library/think/model/relation/OneToOne.php b/thinkphp/library/think/model/relation/OneToOne.php
new file mode 100644
index 00000000..ac5d4e4c
--- /dev/null
+++ b/thinkphp/library/think/model/relation/OneToOne.php
@@ -0,0 +1,342 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\model\relation;
+
+use think\db\Query;
+use think\Exception;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+
+/**
+ * Class OneToOne
+ * @package think\model\relation
+ *
+ */
+abstract class OneToOne extends Relation
+{
+ // 预载入方式 0 -JOIN 1 -IN
+ protected $eagerlyType = 1;
+ // 当前关联的JOIN类型
+ protected $joinType;
+ // 要绑定的属性
+ protected $bindAttr = [];
+ // 关联名
+ protected $relation;
+
+ /**
+ * 设置join类型
+ * @access public
+ * @param string $type JOIN类型
+ * @return $this
+ */
+ public function joinType($type)
+ {
+ $this->joinType = $type;
+ return $this;
+ }
+
+ /**
+ * 预载入关联查询(JOIN方式)
+ * @access public
+ * @param Query $query 查询对象
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @param \Closure $closure 闭包条件
+ * @param bool $first
+ * @return void
+ */
+ public function eagerly(Query $query, $relation, $subRelation, $closure, $first)
+ {
+ $name = Loader::parseName(basename(str_replace('\\', '/', get_class($this->parent))));
+
+ if ($first) {
+ $table = $query->getTable();
+ $query->table([$table => $name]);
+
+ if ($query->getOptions('field')) {
+ $field = $query->getOptions('field');
+ $query->removeOption('field');
+ } else {
+ $field = true;
+ }
+
+ $query->field($field, false, $table, $name);
+ }
+
+ // 预载入封装
+ $joinTable = $this->query->getTable();
+ $joinAlias = $relation;
+ $query->via($joinAlias);
+
+ if ($this instanceof BelongsTo) {
+ $query->join([$joinTable => $joinAlias], $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType);
+ } else {
+ $query->join([$joinTable => $joinAlias], $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType);
+ }
+
+ if ($closure) {
+ // 执行闭包查询
+ $closure($query);
+ // 使用withField指定获取关联的字段,如
+ // $query->where(['id'=>1])->withField('id,name');
+ if ($query->getOptions('with_field')) {
+ $field = $query->getOptions('with_field');
+ $query->removeOption('with_field');
+ } else {
+ $field = true;
+ }
+ } elseif (isset($this->option['field'])) {
+ $field = $this->option['field'];
+ } else {
+ $field = true;
+ }
+
+ $query->field($field, false, $joinTable, $joinAlias, $relation . '__');
+ }
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access protected
+ * @param array $resultSet
+ * @param string $relation
+ * @param string $subRelation
+ * @param \Closure $closure
+ * @return mixed
+ */
+ abstract protected function eagerlySet(&$resultSet, $relation, $subRelation, $closure);
+
+ /**
+ * 预载入关联查询(数据)
+ * @access protected
+ * @param Model $result
+ * @param string $relation
+ * @param string $subRelation
+ * @param \Closure $closure
+ * @return mixed
+ */
+ abstract protected function eagerlyOne(&$result, $relation, $subRelation, $closure);
+
+ /**
+ * 预载入关联查询(数据集)
+ * @access public
+ * @param array $resultSet 数据集
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResultSet(&$resultSet, $relation, $subRelation, $closure)
+ {
+ if (1 == $this->eagerlyType) {
+ // IN查询
+ $this->eagerlySet($resultSet, $relation, $subRelation, $closure);
+ } else {
+ // 模型关联组装
+ foreach ($resultSet as $result) {
+ $this->match($this->model, $relation, $result);
+ }
+ }
+ }
+
+ /**
+ * 预载入关联查询(数据)
+ * @access public
+ * @param Model $result 数据对象
+ * @param string $relation 当前关联名
+ * @param string $subRelation 子关联名
+ * @param \Closure $closure 闭包
+ * @return void
+ */
+ public function eagerlyResult(&$result, $relation, $subRelation, $closure)
+ {
+ if (1 == $this->eagerlyType) {
+ // IN查询
+ $this->eagerlyOne($result, $relation, $subRelation, $closure);
+ } else {
+ // 模型关联组装
+ $this->match($this->model, $relation, $result);
+ }
+ }
+
+ /**
+ * 保存(新增)当前关联数据对象
+ * @access public
+ * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键
+ * @return Model|false
+ */
+ public function save($data)
+ {
+ if ($data instanceof Model) {
+ $data = $data->getData();
+ }
+
+ $model = new $this->model;
+ // 保存关联表数据
+ $data[$this->foreignKey] = $this->parent->{$this->localKey};
+
+ return $model->save($data) ? $model : false;
+ }
+
+ /**
+ * 设置预载入方式
+ * @access public
+ * @param integer $type 预载入方式 0 JOIN查询 1 IN查询
+ * @return $this
+ */
+ public function setEagerlyType($type)
+ {
+ $this->eagerlyType = $type;
+
+ return $this;
+ }
+
+ /**
+ * 获取预载入方式
+ * @access public
+ * @return integer
+ */
+ public function getEagerlyType()
+ {
+ return $this->eagerlyType;
+ }
+
+ /**
+ * 绑定关联表的属性到父模型属性
+ * @access public
+ * @param mixed $attr 要绑定的属性列表
+ * @return $this
+ */
+ public function bind($attr)
+ {
+ if (is_string($attr)) {
+ $attr = explode(',', $attr);
+ }
+ $this->bindAttr = $attr;
+
+ return $this;
+ }
+
+ /**
+ * 获取绑定属性
+ * @access public
+ * @return array
+ */
+ public function getBindAttr()
+ {
+ return $this->bindAttr;
+ }
+
+ /**
+ * 关联统计
+ * @access public
+ * @param Model $result 数据对象
+ * @param \Closure $closure 闭包
+ * @param string $aggregate 聚合查询方法
+ * @param string $field 字段
+ * @return integer
+ */
+ public function relationCount($result, $closure, $aggregate = 'count', $field = '*')
+ {
+ throw new Exception('relation not support: ' . $aggregate);
+ }
+
+ /**
+ * 一对一 关联模型预查询拼装
+ * @access public
+ * @param string $model 模型名称
+ * @param string $relation 关联名
+ * @param Model $result 模型对象实例
+ * @return void
+ */
+ protected function match($model, $relation, &$result)
+ {
+ // 重新组装模型数据
+ foreach ($result->getData() as $key => $val) {
+ if (strpos($key, '__')) {
+ list($name, $attr) = explode('__', $key, 2);
+ if ($name == $relation) {
+ $list[$name][$attr] = $val;
+ unset($result->$key);
+ }
+ }
+ }
+
+ if (isset($list[$relation])) {
+ $relationModel = new $model($list[$relation]);
+ $relationModel->setParent(clone $result);
+ $relationModel->isUpdate(true);
+
+ if (!empty($this->bindAttr)) {
+ $this->bindAttr($relationModel, $result, $this->bindAttr);
+ }
+ } else {
+ $relationModel = null;
+ }
+
+ $result->setRelation(Loader::parseName($relation), $relationModel);
+ }
+
+ /**
+ * 绑定关联属性到父模型
+ * @access protected
+ * @param Model $model 关联模型对象
+ * @param Model $result 父模型对象
+ * @return void
+ * @throws Exception
+ */
+ protected function bindAttr($model, &$result)
+ {
+ foreach ($this->bindAttr as $key => $attr) {
+ $key = is_numeric($key) ? $attr : $key;
+ if (isset($result->$key)) {
+ throw new Exception('bind attr has exists:' . $key);
+ } else {
+ $result->setAttr($key, $model ? $model->$attr : null);
+ }
+ }
+ }
+
+ /**
+ * 一对一 关联模型预查询(IN方式)
+ * @access public
+ * @param array $where 关联预查询条件
+ * @param string $key 关联键名
+ * @param string $relation 关联名
+ * @param string $subRelation 子关联
+ * @param \Closure $closure
+ * @return array
+ */
+ protected function eagerlyWhere($where, $key, $relation, $subRelation = '', $closure = null)
+ {
+ // 预载入关联查询 支持嵌套预载入
+ if ($closure) {
+ $closure($this->query);
+
+ if ($field = $this->query->getOptions('with_field')) {
+ $this->query->field($field)->removeOption('with_field');
+ }
+ }
+
+ $list = $this->query->where($where)->with($subRelation)->select();
+
+ // 组装模型数据
+ $data = [];
+
+ foreach ($list as $set) {
+ $data[$set->$key] = $set;
+ }
+
+ return $data;
+ }
+
+}
diff --git a/thinkphp/library/think/paginator/driver/Bootstrap.php b/thinkphp/library/think/paginator/driver/Bootstrap.php
new file mode 100644
index 00000000..ab5315c0
--- /dev/null
+++ b/thinkphp/library/think/paginator/driver/Bootstrap.php
@@ -0,0 +1,206 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\paginator\driver;
+
+use think\Paginator;
+
+class Bootstrap extends Paginator
+{
+
+ /**
+ * 上一页按钮
+ * @param string $text
+ * @return string
+ */
+ protected function getPreviousButton($text = "«")
+ {
+
+ if ($this->currentPage() <= 1) {
+ return $this->getDisabledTextWrapper($text);
+ }
+
+ $url = $this->url(
+ $this->currentPage() - 1
+ );
+
+ return $this->getPageLinkWrapper($url, $text);
+ }
+
+ /**
+ * 下一页按钮
+ * @param string $text
+ * @return string
+ */
+ protected function getNextButton($text = '»')
+ {
+ if (!$this->hasMore) {
+ return $this->getDisabledTextWrapper($text);
+ }
+
+ $url = $this->url($this->currentPage() + 1);
+
+ return $this->getPageLinkWrapper($url, $text);
+ }
+
+ /**
+ * 页码按钮
+ * @return string
+ */
+ protected function getLinks()
+ {
+ if ($this->simple) {
+ return '';
+ }
+
+ $block = [
+ 'first' => null,
+ 'slider' => null,
+ 'last' => null,
+ ];
+
+ $side = 3;
+ $window = $side * 2;
+
+ if ($this->lastPage < $window + 6) {
+ $block['first'] = $this->getUrlRange(1, $this->lastPage);
+ } elseif ($this->currentPage <= $window) {
+ $block['first'] = $this->getUrlRange(1, $window + 2);
+ $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+ } elseif ($this->currentPage > ($this->lastPage - $window)) {
+ $block['first'] = $this->getUrlRange(1, 2);
+ $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
+ } else {
+ $block['first'] = $this->getUrlRange(1, 2);
+ $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
+ $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+ }
+
+ $html = '';
+
+ if (is_array($block['first'])) {
+ $html .= $this->getUrlLinks($block['first']);
+ }
+
+ if (is_array($block['slider'])) {
+ $html .= $this->getDots();
+ $html .= $this->getUrlLinks($block['slider']);
+ }
+
+ if (is_array($block['last'])) {
+ $html .= $this->getDots();
+ $html .= $this->getUrlLinks($block['last']);
+ }
+
+ return $html;
+ }
+
+ /**
+ * 渲染分页html
+ * @return mixed
+ */
+ public function render()
+ {
+ if ($this->hasPages()) {
+ if ($this->simple) {
+ return sprintf(
+ '',
+ $this->getPreviousButton(),
+ $this->getNextButton()
+ );
+ } else {
+ return sprintf(
+ '',
+ $this->getPreviousButton(),
+ $this->getLinks(),
+ $this->getNextButton()
+ );
+ }
+ }
+ }
+
+ /**
+ * 生成一个可点击的按钮
+ *
+ * @param string $url
+ * @param int $page
+ * @return string
+ */
+ protected function getAvailablePageWrapper($url, $page)
+ {
+ return '' . $page . ' ';
+ }
+
+ /**
+ * 生成一个禁用的按钮
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function getDisabledTextWrapper($text)
+ {
+ return '' . $text . ' ';
+ }
+
+ /**
+ * 生成一个激活的按钮
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function getActivePageWrapper($text)
+ {
+ return '' . $text . ' ';
+ }
+
+ /**
+ * 生成省略号按钮
+ *
+ * @return string
+ */
+ protected function getDots()
+ {
+ return $this->getDisabledTextWrapper('...');
+ }
+
+ /**
+ * 批量生成页码按钮.
+ *
+ * @param array $urls
+ * @return string
+ */
+ protected function getUrlLinks(array $urls)
+ {
+ $html = '';
+
+ foreach ($urls as $page => $url) {
+ $html .= $this->getPageLinkWrapper($url, $page);
+ }
+
+ return $html;
+ }
+
+ /**
+ * 生成普通页码按钮
+ *
+ * @param string $url
+ * @param int $page
+ * @return string
+ */
+ protected function getPageLinkWrapper($url, $page)
+ {
+ if ($this->currentPage() == $page) {
+ return $this->getActivePageWrapper($page);
+ }
+
+ return $this->getAvailablePageWrapper($url, $page);
+ }
+}
diff --git a/thinkphp/library/think/process/Builder.php b/thinkphp/library/think/process/Builder.php
new file mode 100644
index 00000000..da561639
--- /dev/null
+++ b/thinkphp/library/think/process/Builder.php
@@ -0,0 +1,233 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process;
+
+use think\Process;
+
+class Builder
+{
+ private $arguments;
+ private $cwd;
+ private $env = null;
+ private $input;
+ private $timeout = 60;
+ private $options = [];
+ private $inheritEnv = true;
+ private $prefix = [];
+ private $outputDisabled = false;
+
+ /**
+ * 构造方法
+ * @param string[] $arguments 参数
+ */
+ public function __construct(array $arguments = [])
+ {
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * 创建一个实例
+ * @param string[] $arguments 参数
+ * @return self
+ */
+ public static function create(array $arguments = [])
+ {
+ return new static($arguments);
+ }
+
+ /**
+ * 添加一个参数
+ * @param string $argument 参数
+ * @return self
+ */
+ public function add($argument)
+ {
+ $this->arguments[] = $argument;
+
+ return $this;
+ }
+
+ /**
+ * 添加一个前缀
+ * @param string|array $prefix
+ * @return self
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = is_array($prefix) ? $prefix : [$prefix];
+
+ return $this;
+ }
+
+ /**
+ * 设置参数
+ * @param string[] $arguments
+ * @return self
+ */
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+
+ return $this;
+ }
+
+ /**
+ * 设置工作目录
+ * @param null|string $cwd
+ * @return self
+ */
+ public function setWorkingDirectory($cwd)
+ {
+ $this->cwd = $cwd;
+
+ return $this;
+ }
+
+ /**
+ * 是否初始化环境变量
+ * @param bool $inheritEnv
+ * @return self
+ */
+ public function inheritEnvironmentVariables($inheritEnv = true)
+ {
+ $this->inheritEnv = $inheritEnv;
+
+ return $this;
+ }
+
+ /**
+ * 设置环境变量
+ * @param string $name
+ * @param null|string $value
+ * @return self
+ */
+ public function setEnv($name, $value)
+ {
+ $this->env[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 添加环境变量
+ * @param array $variables
+ * @return self
+ */
+ public function addEnvironmentVariables(array $variables)
+ {
+ $this->env = array_replace($this->env, $variables);
+
+ return $this;
+ }
+
+ /**
+ * 设置输入
+ * @param mixed $input
+ * @return self
+ */
+ public function setInput($input)
+ {
+ $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input);
+
+ return $this;
+ }
+
+ /**
+ * 设置超时时间
+ * @param float|null $timeout
+ * @return self
+ */
+ public function setTimeout($timeout)
+ {
+ if (null === $timeout) {
+ $this->timeout = null;
+
+ return $this;
+ }
+
+ $timeout = (float) $timeout;
+
+ if ($timeout < 0) {
+ throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
+ }
+
+ $this->timeout = $timeout;
+
+ return $this;
+ }
+
+ /**
+ * 设置proc_open选项
+ * @param string $name
+ * @param string $value
+ * @return self
+ */
+ public function setOption($name, $value)
+ {
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * 禁止输出
+ * @return self
+ */
+ public function disableOutput()
+ {
+ $this->outputDisabled = true;
+
+ return $this;
+ }
+
+ /**
+ * 开启输出
+ * @return self
+ */
+ public function enableOutput()
+ {
+ $this->outputDisabled = false;
+
+ return $this;
+ }
+
+ /**
+ * 创建一个Process实例
+ * @return Process
+ */
+ public function getProcess()
+ {
+ if (0 === count($this->prefix) && 0 === count($this->arguments)) {
+ throw new \LogicException('You must add() command arguments before calling getProcess().');
+ }
+
+ $options = $this->options;
+
+ $arguments = array_merge($this->prefix, $this->arguments);
+ $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments));
+
+ if ($this->inheritEnv) {
+ // include $_ENV for BC purposes
+ $env = array_replace($_ENV, $_SERVER, $this->env);
+ } else {
+ $env = $this->env;
+ }
+
+ $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options);
+
+ if ($this->outputDisabled) {
+ $process->disableOutput();
+ }
+
+ return $process;
+ }
+}
diff --git a/thinkphp/library/think/process/Utils.php b/thinkphp/library/think/process/Utils.php
new file mode 100644
index 00000000..f94c6488
--- /dev/null
+++ b/thinkphp/library/think/process/Utils.php
@@ -0,0 +1,75 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process;
+
+class Utils
+{
+
+ /**
+ * 转义字符串
+ * @param string $argument
+ * @return string
+ */
+ public static function escapeArgument($argument)
+ {
+
+ if ('' === $argument) {
+ return escapeshellarg($argument);
+ }
+ $escapedArgument = '';
+ $quote = false;
+ foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
+ if ('"' === $part) {
+ $escapedArgument .= '\\"';
+ } elseif (self::isSurroundedBy($part, '%')) {
+ // Avoid environment variable expansion
+ $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%';
+ } else {
+ // escape trailing backslash
+ if ('\\' === substr($part, -1)) {
+ $part .= '\\';
+ }
+ $quote = true;
+ $escapedArgument .= $part;
+ }
+ }
+ if ($quote) {
+ $escapedArgument = '"' . $escapedArgument . '"';
+ }
+ return $escapedArgument;
+ }
+
+ /**
+ * 验证并进行规范化Process输入。
+ * @param string $caller
+ * @param mixed $input
+ * @return string
+ * @throws \InvalidArgumentException
+ */
+ public static function validateInput($caller, $input)
+ {
+ if (null !== $input) {
+ if (is_resource($input)) {
+ return $input;
+ }
+ if (is_scalar($input)) {
+ return (string) $input;
+ }
+ throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller));
+ }
+ return $input;
+ }
+
+ private static function isSurroundedBy($arg, $char)
+ {
+ return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
+ }
+
+}
diff --git a/thinkphp/library/think/process/exception/Faild.php b/thinkphp/library/think/process/exception/Faild.php
new file mode 100644
index 00000000..38647bc1
--- /dev/null
+++ b/thinkphp/library/think/process/exception/Faild.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\exception;
+
+use think\Process;
+
+class Faild extends \RuntimeException
+{
+
+ private $process;
+
+ public function __construct(Process $process)
+ {
+ if ($process->isSuccessful()) {
+ throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.');
+ }
+
+ $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText());
+
+ if (!$process->isOutputDisabled()) {
+ $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput());
+ }
+
+ parent::__construct($error);
+
+ $this->process = $process;
+ }
+
+ public function getProcess()
+ {
+ return $this->process;
+ }
+}
diff --git a/thinkphp/library/think/process/exception/Failed.php b/thinkphp/library/think/process/exception/Failed.php
new file mode 100644
index 00000000..52950823
--- /dev/null
+++ b/thinkphp/library/think/process/exception/Failed.php
@@ -0,0 +1,42 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\exception;
+
+use think\Process;
+
+class Failed extends \RuntimeException
+{
+
+ private $process;
+
+ public function __construct(Process $process)
+ {
+ if ($process->isSuccessful()) {
+ throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.');
+ }
+
+ $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText());
+
+ if (!$process->isOutputDisabled()) {
+ $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput());
+ }
+
+ parent::__construct($error);
+
+ $this->process = $process;
+ }
+
+ public function getProcess()
+ {
+ return $this->process;
+ }
+}
diff --git a/thinkphp/library/think/process/exception/Timeout.php b/thinkphp/library/think/process/exception/Timeout.php
new file mode 100644
index 00000000..d5f1162f
--- /dev/null
+++ b/thinkphp/library/think/process/exception/Timeout.php
@@ -0,0 +1,61 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\exception;
+
+use think\Process;
+
+class Timeout extends \RuntimeException
+{
+
+ const TYPE_GENERAL = 1;
+ const TYPE_IDLE = 2;
+
+ private $process;
+ private $timeoutType;
+
+ public function __construct(Process $process, $timeoutType)
+ {
+ $this->process = $process;
+ $this->timeoutType = $timeoutType;
+
+ parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout()));
+ }
+
+ public function getProcess()
+ {
+ return $this->process;
+ }
+
+ public function isGeneralTimeout()
+ {
+ return $this->timeoutType === self::TYPE_GENERAL;
+ }
+
+ public function isIdleTimeout()
+ {
+ return $this->timeoutType === self::TYPE_IDLE;
+ }
+
+ public function getExceededTimeout()
+ {
+ switch ($this->timeoutType) {
+ case self::TYPE_GENERAL:
+ return $this->process->getTimeout();
+
+ case self::TYPE_IDLE:
+ return $this->process->getIdleTimeout();
+
+ default:
+ throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
+ }
+ }
+}
diff --git a/thinkphp/library/think/process/pipes/Pipes.php b/thinkphp/library/think/process/pipes/Pipes.php
new file mode 100644
index 00000000..82396b8f
--- /dev/null
+++ b/thinkphp/library/think/process/pipes/Pipes.php
@@ -0,0 +1,93 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+abstract class Pipes
+{
+
+ /** @var array */
+ public $pipes = [];
+
+ /** @var string */
+ protected $inputBuffer = '';
+ /** @var resource|null */
+ protected $input;
+
+ /** @var bool */
+ private $blocked = true;
+
+ const CHUNK_SIZE = 16384;
+
+ /**
+ * 返回用于 proc_open 描述符的数组
+ * @return array
+ */
+ abstract public function getDescriptors();
+
+ /**
+ * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。
+ * @return string[]
+ */
+ abstract public function getFiles();
+
+ /**
+ * 文件句柄和管道中读取数据。
+ * @param bool $blocking 是否使用阻塞调用
+ * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。
+ * @return string[]
+ */
+ abstract public function readAndWrite($blocking, $close = false);
+
+ /**
+ * 返回当前状态如果有打开的文件句柄或管道。
+ * @return bool
+ */
+ abstract public function areOpen();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function close()
+ {
+ foreach ($this->pipes as $pipe) {
+ fclose($pipe);
+ }
+ $this->pipes = [];
+ }
+
+ /**
+ * 检查系统调用已被中断
+ * @return bool
+ */
+ protected function hasSystemCallBeenInterrupted()
+ {
+ $lastError = error_get_last();
+
+ return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
+ }
+
+ protected function unblock()
+ {
+ if (!$this->blocked) {
+ return;
+ }
+
+ foreach ($this->pipes as $pipe) {
+ stream_set_blocking($pipe, 0);
+ }
+ if (null !== $this->input) {
+ stream_set_blocking($this->input, 0);
+ }
+
+ $this->blocked = false;
+ }
+}
diff --git a/thinkphp/library/think/process/pipes/Unix.php b/thinkphp/library/think/process/pipes/Unix.php
new file mode 100644
index 00000000..fd99a5d6
--- /dev/null
+++ b/thinkphp/library/think/process/pipes/Unix.php
@@ -0,0 +1,196 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+use think\Process;
+
+class Unix extends Pipes
+{
+
+ /** @var bool */
+ private $ttyMode;
+ /** @var bool */
+ private $ptyMode;
+ /** @var bool */
+ private $disableOutput;
+
+ public function __construct($ttyMode, $ptyMode, $input, $disableOutput)
+ {
+ $this->ttyMode = (bool) $ttyMode;
+ $this->ptyMode = (bool) $ptyMode;
+ $this->disableOutput = (bool) $disableOutput;
+
+ if (is_resource($input)) {
+ $this->input = $input;
+ } else {
+ $this->inputBuffer = (string) $input;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescriptors()
+ {
+ if ($this->disableOutput) {
+ $nullstream = fopen('/dev/null', 'c');
+
+ return [
+ ['pipe', 'r'],
+ $nullstream,
+ $nullstream,
+ ];
+ }
+
+ if ($this->ttyMode) {
+ return [
+ ['file', '/dev/tty', 'r'],
+ ['file', '/dev/tty', 'w'],
+ ['file', '/dev/tty', 'w'],
+ ];
+ }
+
+ if ($this->ptyMode && Process::isPtySupported()) {
+ return [
+ ['pty'],
+ ['pty'],
+ ['pty'],
+ ];
+ }
+
+ return [
+ ['pipe', 'r'],
+ ['pipe', 'w'], // stdout
+ ['pipe', 'w'], // stderr
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFiles()
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readAndWrite($blocking, $close = false)
+ {
+
+ if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+
+ if (empty($this->pipes)) {
+ return [];
+ }
+
+ $this->unblock();
+
+ $read = [];
+
+ if (null !== $this->input) {
+ $r = array_merge($this->pipes, ['input' => $this->input]);
+ } else {
+ $r = $this->pipes;
+ }
+
+ unset($r[0]);
+
+ $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null;
+ $e = null;
+
+ if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
+
+ if (!$this->hasSystemCallBeenInterrupted()) {
+ $this->pipes = [];
+ }
+
+ return $read;
+ }
+
+ if (0 === $n) {
+ return $read;
+ }
+
+ foreach ($r as $pipe) {
+
+ $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input';
+ $data = '';
+ while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+
+ if ('' !== $data) {
+ if ('input' === $type) {
+ $this->inputBuffer .= $data;
+ } else {
+ $read[$type] = $data;
+ }
+ }
+
+ if (false === $data || (true === $close && feof($pipe) && '' === $data)) {
+ if ('input' === $type) {
+ $this->input = null;
+ } else {
+ fclose($this->pipes[$type]);
+ unset($this->pipes[$type]);
+ }
+ }
+ }
+
+ if (null !== $w && 0 < count($w)) {
+ while (strlen($this->inputBuffer)) {
+ $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k
+ if ($written > 0) {
+ $this->inputBuffer = (string) substr($this->inputBuffer, $written);
+ } else {
+ break;
+ }
+ }
+ }
+
+ if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+
+ return $read;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function areOpen()
+ {
+ return (bool) $this->pipes;
+ }
+
+ /**
+ * 创建一个新的 UnixPipes 实例
+ * @param Process $process
+ * @param string|resource $input
+ * @return self
+ */
+ public static function create(Process $process, $input)
+ {
+ return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled());
+ }
+}
diff --git a/thinkphp/library/think/process/pipes/Windows.php b/thinkphp/library/think/process/pipes/Windows.php
new file mode 100644
index 00000000..1b8b0d4f
--- /dev/null
+++ b/thinkphp/library/think/process/pipes/Windows.php
@@ -0,0 +1,228 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\process\pipes;
+
+use think\Process;
+
+class Windows extends Pipes
+{
+
+ /** @var array */
+ private $files = [];
+ /** @var array */
+ private $fileHandles = [];
+ /** @var array */
+ private $readBytes = [
+ Process::STDOUT => 0,
+ Process::STDERR => 0,
+ ];
+ /** @var bool */
+ private $disableOutput;
+
+ public function __construct($disableOutput, $input)
+ {
+ $this->disableOutput = (bool) $disableOutput;
+
+ if (!$this->disableOutput) {
+
+ $this->files = [
+ Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'),
+ Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'),
+ ];
+ foreach ($this->files as $offset => $file) {
+ $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb');
+ if (false === $this->fileHandles[$offset]) {
+ throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
+ }
+ }
+ }
+
+ if (is_resource($input)) {
+ $this->input = $input;
+ } else {
+ $this->inputBuffer = $input;
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ $this->removeFiles();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescriptors()
+ {
+ if ($this->disableOutput) {
+ $nullstream = fopen('NUL', 'c');
+
+ return [
+ ['pipe', 'r'],
+ $nullstream,
+ $nullstream,
+ ];
+ }
+
+ return [
+ ['pipe', 'r'],
+ ['file', 'NUL', 'w'],
+ ['file', 'NUL', 'w'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFiles()
+ {
+ return $this->files;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readAndWrite($blocking, $close = false)
+ {
+ $this->write($blocking, $close);
+
+ $read = [];
+ $fh = $this->fileHandles;
+ foreach ($fh as $type => $fileHandle) {
+ if (0 !== fseek($fileHandle, $this->readBytes[$type])) {
+ continue;
+ }
+ $data = '';
+ $dataread = null;
+ while (!feof($fileHandle)) {
+ if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+ }
+ if (0 < $length = strlen($data)) {
+ $this->readBytes[$type] += $length;
+ $read[$type] = $data;
+ }
+
+ if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) {
+ fclose($this->fileHandles[$type]);
+ unset($this->fileHandles[$type]);
+ }
+ }
+
+ return $read;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function areOpen()
+ {
+ return (bool) $this->pipes && (bool) $this->fileHandles;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function close()
+ {
+ parent::close();
+ foreach ($this->fileHandles as $handle) {
+ fclose($handle);
+ }
+ $this->fileHandles = [];
+ }
+
+ /**
+ * 创建一个新的 WindowsPipes 实例。
+ * @param Process $process
+ * @param $input
+ * @return self
+ */
+ public static function create(Process $process, $input)
+ {
+ return new static($process->isOutputDisabled(), $input);
+ }
+
+ /**
+ * 删除临时文件
+ */
+ private function removeFiles()
+ {
+ foreach ($this->files as $filename) {
+ if (file_exists($filename)) {
+ @unlink($filename);
+ }
+ }
+ $this->files = [];
+ }
+
+ /**
+ * 写入到 stdin 输入
+ * @param bool $blocking
+ * @param bool $close
+ */
+ private function write($blocking, $close)
+ {
+ if (empty($this->pipes)) {
+ return;
+ }
+
+ $this->unblock();
+
+ $r = null !== $this->input ? ['input' => $this->input] : null;
+ $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null;
+ $e = null;
+
+ if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
+ if (!$this->hasSystemCallBeenInterrupted()) {
+ $this->pipes = [];
+ }
+
+ return;
+ }
+
+ if (0 === $n) {
+ return;
+ }
+
+ if (null !== $r && 0 < count($r)) {
+ $data = '';
+ while ($dataread = fread($r['input'], self::CHUNK_SIZE)) {
+ $data .= $dataread;
+ }
+
+ $this->inputBuffer .= $data;
+
+ if (false === $data || (true === $close && feof($r['input']) && '' === $data)) {
+ $this->input = null;
+ }
+ }
+
+ if (null !== $w && 0 < count($w)) {
+ while (strlen($this->inputBuffer)) {
+ $written = fwrite($w[0], $this->inputBuffer, 2 << 18);
+ if ($written > 0) {
+ $this->inputBuffer = (string) substr($this->inputBuffer, $written);
+ } else {
+ break;
+ }
+ }
+ }
+
+ if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) {
+ fclose($this->pipes[0]);
+ unset($this->pipes[0]);
+ }
+ }
+}
diff --git a/thinkphp/library/think/response/Json.php b/thinkphp/library/think/response/Json.php
new file mode 100644
index 00000000..aa5bbd6f
--- /dev/null
+++ b/thinkphp/library/think/response/Json.php
@@ -0,0 +1,51 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Response;
+
+class Json extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/json';
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function output($data)
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/response/Jsonp.php b/thinkphp/library/think/response/Jsonp.php
new file mode 100644
index 00000000..f69e88e1
--- /dev/null
+++ b/thinkphp/library/think/response/Jsonp.php
@@ -0,0 +1,58 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Response;
+
+class Jsonp extends Response
+{
+ // 输出参数
+ protected $options = [
+ 'var_jsonp_handler' => 'callback',
+ 'default_jsonp_handler' => 'jsonpReturn',
+ 'json_encode_param' => JSON_UNESCAPED_UNICODE,
+ ];
+
+ protected $contentType = 'application/javascript';
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function output($data)
+ {
+ try {
+ // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取]
+ $var_jsonp_handler = $this->app['request']->param($this->options['var_jsonp_handler'], "");
+ $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler'];
+
+ $data = json_encode($data, $this->options['json_encode_param']);
+
+ if (false === $data) {
+ throw new \InvalidArgumentException(json_last_error_msg());
+ }
+
+ $data = $handler . '(' . $data . ');';
+
+ return $data;
+ } catch (\Exception $e) {
+ if ($e->getPrevious()) {
+ throw $e->getPrevious();
+ }
+ throw $e;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/response/Jump.php b/thinkphp/library/think/response/Jump.php
new file mode 100644
index 00000000..258448ca
--- /dev/null
+++ b/thinkphp/library/think/response/Jump.php
@@ -0,0 +1,32 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Response;
+
+class Jump extends Response
+{
+ protected $contentType = 'text/html';
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function output($data)
+ {
+ $data = $this->app['view']->fetch($this->options['jump_template'], $data);
+ return $data;
+ }
+}
diff --git a/thinkphp/library/think/response/Redirect.php b/thinkphp/library/think/response/Redirect.php
new file mode 100644
index 00000000..62fa83a7
--- /dev/null
+++ b/thinkphp/library/think/response/Redirect.php
@@ -0,0 +1,115 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Response;
+
+class Redirect extends Response
+{
+
+ protected $options = [];
+
+ // URL参数
+ protected $params = [];
+
+ public function __construct($data = '', $code = 302, array $header = [], array $options = [])
+ {
+ parent::__construct($data, $code, $header, $options);
+
+ $this->cacheControl('no-cache,must-revalidate');
+ }
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data)
+ {
+ $this->header['Location'] = $this->getTargetUrl();
+
+ return;
+ }
+
+ /**
+ * 重定向传值(通过Session)
+ * @access protected
+ * @param string|array $name 变量名或者数组
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function with($name, $value = null)
+ {
+ $session = $this->app['session'];
+
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $session->flash($key, $val);
+ }
+ } else {
+ $session->flash($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取跳转地址
+ * @access public
+ * @return string
+ */
+ public function getTargetUrl()
+ {
+ if (strpos($this->data, '://') || (0 === strpos($this->data, '/') && empty($this->params))) {
+ return $this->data;
+ } else {
+ return $this->app['url']->build($this->data, $this->params);
+ }
+ }
+
+ public function params($params = [])
+ {
+ $this->params = $params;
+
+ return $this;
+ }
+
+ /**
+ * 记住当前url后跳转
+ * @access public
+ * @return $this
+ */
+ public function remember()
+ {
+ $this->app['session']->set('redirect_url', $this->app['request']->url());
+
+ return $this;
+ }
+
+ /**
+ * 跳转到上次记住的url
+ * @access public
+ * @return $this
+ */
+ public function restore()
+ {
+ $session = $this->app['session'];
+
+ if ($session->has('redirect_url')) {
+ $this->data = $session->get('redirect_url');
+ $session->delete('redirect_url');
+ }
+
+ return $this;
+ }
+}
diff --git a/thinkphp/library/think/response/View.php b/thinkphp/library/think/response/View.php
new file mode 100644
index 00000000..c836ccb5
--- /dev/null
+++ b/thinkphp/library/think/response/View.php
@@ -0,0 +1,94 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Response;
+
+class View extends Response
+{
+ // 输出参数
+ protected $options = [];
+ protected $vars = [];
+ protected $filter;
+ protected $contentType = 'text/html';
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data)
+ {
+ // 渲染模板输出
+ return $this->app['view']
+ ->filter($this->filter)
+ ->fetch($data, $this->vars);
+ }
+
+ /**
+ * 获取视图变量
+ * @access public
+ * @param string $name 模板变量
+ * @return mixed
+ */
+ public function getVars($name = null)
+ {
+ if (is_null($name)) {
+ return $this->vars;
+ } else {
+ return isset($this->vars[$name]) ? $this->vars[$name] : null;
+ }
+ }
+
+ /**
+ * 模板变量赋值
+ * @access public
+ * @param mixed $name 变量名
+ * @param mixed $value 变量值
+ * @return $this
+ */
+ public function assign($name, $value = '')
+ {
+ if (is_array($name)) {
+ $this->vars = array_merge($this->vars, $name);
+ } else {
+ $this->vars[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 视图内容过滤
+ * @access public
+ * @param callable $filter
+ * @return $this
+ */
+ public function filter($filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * 检查模板是否存在
+ * @access private
+ * @param string|array $name 参数名
+ * @return bool
+ */
+ public function exists($name)
+ {
+ return $this->app['view']->exists($name);
+ }
+
+}
diff --git a/thinkphp/library/think/response/Xml.php b/thinkphp/library/think/response/Xml.php
new file mode 100644
index 00000000..9c1681a4
--- /dev/null
+++ b/thinkphp/library/think/response/Xml.php
@@ -0,0 +1,116 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\response;
+
+use think\Collection;
+use think\Model;
+use think\Response;
+
+class Xml extends Response
+{
+ // 输出参数
+ protected $options = [
+ // 根节点名
+ 'root_node' => 'think',
+ // 根节点属性
+ 'root_attr' => '',
+ //数字索引的子节点名
+ 'item_node' => 'item',
+ // 数字索引子节点key转换的属性名
+ 'item_key' => 'id',
+ // 数据编码
+ 'encoding' => 'utf-8',
+ ];
+
+ protected $contentType = 'text/xml';
+
+ /**
+ * 处理数据
+ * @access protected
+ * @param mixed $data 要处理的数据
+ * @return mixed
+ */
+ protected function output($data)
+ {
+ if (is_string($data)) {
+ if (0 !== strpos($data, 'options['encoding'];
+ $xml = "";
+ $data = $xml . $data;
+ }
+ return $data;
+ }
+
+ // XML数据转换
+ return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']);
+ }
+
+ /**
+ * XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $root 根节点名
+ * @param string $item 数字索引的子节点名
+ * @param string $attr 根节点属性
+ * @param string $id 数字索引子节点key转换的属性名
+ * @param string $encoding 数据编码
+ * @return string
+ */
+ protected function xmlEncode($data, $root, $item, $attr, $id, $encoding)
+ {
+ if (is_array($attr)) {
+ $array = [];
+ foreach ($attr as $key => $value) {
+ $array[] = "{$key}=\"{$value}\"";
+ }
+ $attr = implode(' ', $array);
+ }
+
+ $attr = trim($attr);
+ $attr = empty($attr) ? '' : " {$attr}";
+ $xml = "";
+ $xml .= "<{$root}{$attr}>";
+ $xml .= $this->dataToXml($data, $item, $id);
+ $xml .= "{$root}>";
+
+ return $xml;
+ }
+
+ /**
+ * 数据XML编码
+ * @access protected
+ * @param mixed $data 数据
+ * @param string $item 数字索引时的节点名称
+ * @param string $id 数字索引key转换为的属性名
+ * @return string
+ */
+ protected function dataToXml($data, $item, $id)
+ {
+ $xml = $attr = '';
+
+ if ($data instanceof Collection || $data instanceof Model) {
+ $data = $data->toArray();
+ }
+
+ foreach ($data as $key => $val) {
+ if (is_numeric($key)) {
+ $id && $attr = " {$id}=\"{$key}\"";
+ $key = $item;
+ }
+ $xml .= "<{$key}{$attr}>";
+ $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val;
+ $xml .= "{$key}>";
+ }
+
+ return $xml;
+ }
+}
diff --git a/thinkphp/library/think/route/AliasRule.php b/thinkphp/library/think/route/AliasRule.php
new file mode 100644
index 00000000..daf59fe1
--- /dev/null
+++ b/thinkphp/library/think/route/AliasRule.php
@@ -0,0 +1,114 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Route;
+
+class AliasRule extends Domain
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由实例
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 路由别名
+ * @param string $route 路由绑定
+ * @param array $option 路由参数
+ */
+ public function __construct(Route $router, RuleGroup $parent, $name, $route, $option = [])
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->name = $name;
+ $this->route = $route;
+ $this->option = $option;
+ }
+
+ /**
+ * 检测路由别名
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check($request, $url, $completeMatch = false)
+ {
+ if ($dispatch = $this->checkCrossDomain($request)) {
+ // 允许跨域
+ return $dispatch;
+ }
+
+ // 检查参数有效性
+ if (!$this->checkOption($this->option, $request)) {
+ return false;
+ }
+
+ list($action, $bind) = array_pad(explode('|', $url, 2), 2, '');
+
+ if (isset($this->option['allow']) && !in_array($action, $this->option['allow'])) {
+ // 允许操作
+ return false;
+ } elseif (isset($this->option['except']) && in_array($action, $this->option['except'])) {
+ // 排除操作
+ return false;
+ }
+
+ if (isset($this->option['method'][$action])) {
+ $this->option['method'] = $this->option['method'][$action];
+ }
+
+ // 匹配后执行的行为
+ $this->afterMatchGroup($request);
+
+ if ($this->parent) {
+ // 合并分组参数
+ $this->mergeGroupOptions();
+ }
+
+ $this->parseBindAppendParam($this->route);
+
+ if (0 === strpos($this->route, '\\')) {
+ // 路由到类
+ return $this->bindToClass($request, $bind, substr($this->route, 1));
+ } elseif (0 === strpos($this->route, '@')) {
+ // 路由到控制器类
+ return $this->bindToController($request, $bind, substr($this->route, 1));
+ } else {
+ // 路由到模块/控制器
+ return $this->bindToModule($request, $bind, $this->route);
+ }
+ }
+
+ /**
+ * 设置允许的操作方法
+ * @access public
+ * @param array $action 操作方法
+ * @return $this
+ */
+ public function allow($action = [])
+ {
+ return $this->option('allow', $action);
+ }
+
+ /**
+ * 设置排除的操作方法
+ * @access public
+ * @param array $action 操作方法
+ * @return $this
+ */
+ public function except($action = [])
+ {
+ return $this->option('except', $action);
+ }
+
+}
diff --git a/thinkphp/library/think/route/Dispatch.php b/thinkphp/library/think/route/Dispatch.php
new file mode 100644
index 00000000..4b187d1e
--- /dev/null
+++ b/thinkphp/library/think/route/Dispatch.php
@@ -0,0 +1,355 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Container;
+use think\exception\ValidateException;
+use think\Request;
+use think\Response;
+
+abstract class Dispatch
+{
+ /**
+ * 应用对象
+ * @var App
+ */
+ protected $app;
+
+ /**
+ * 请求对象
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * 路由规则
+ * @var Rule
+ */
+ protected $rule;
+
+ /**
+ * 调度信息
+ * @var mixed
+ */
+ protected $dispatch;
+
+ /**
+ * 调度参数
+ * @var array
+ */
+ protected $param;
+
+ /**
+ * 状态码
+ * @var string
+ */
+ protected $code;
+
+ /**
+ * 是否进行大小写转换
+ * @var bool
+ */
+ protected $convert;
+
+ public function __construct(Request $request, Rule $rule, $dispatch, $param = [], $code = null)
+ {
+ $this->request = $request;
+ $this->rule = $rule;
+ $this->app = Container::get('app');
+ $this->dispatch = $dispatch;
+ $this->param = $param;
+ $this->code = $code;
+
+ if (isset($param['convert'])) {
+ $this->convert = $param['convert'];
+ }
+ }
+
+ public function init()
+ {
+ // 执行路由后置操作
+ if ($this->rule->doAfter()) {
+ // 设置请求的路由信息
+
+ // 设置当前请求的参数
+ $this->request->setRouteVars($this->rule->getVars());
+ $this->request->routeInfo([
+ 'rule' => $this->rule->getRule(),
+ 'route' => $this->rule->getRoute(),
+ 'option' => $this->rule->getOption(),
+ 'var' => $this->rule->getVars(),
+ ]);
+
+ $this->doRouteAfter();
+ }
+
+ return $this;
+ }
+
+ /**
+ * 检查路由后置操作
+ * @access protected
+ * @return void
+ */
+ protected function doRouteAfter()
+ {
+ // 记录匹配的路由信息
+ $option = $this->rule->getOption();
+ $matches = $this->rule->getVars();
+
+ // 添加中间件
+ if (!empty($option['middleware'])) {
+ $this->app['middleware']->import($option['middleware']);
+ }
+
+ // 绑定模型数据
+ if (!empty($option['model'])) {
+ $this->createBindModel($option['model'], $matches);
+ }
+
+ // 指定Header数据
+ if (!empty($option['header'])) {
+ $header = $option['header'];
+ $this->app['hook']->add('response_send', function ($response) use ($header) {
+ $response->header($header);
+ });
+ }
+
+ // 指定Response响应数据
+ if (!empty($option['response'])) {
+ foreach ($option['response'] as $response) {
+ $this->app['hook']->add('response_send', $response);
+ }
+ }
+
+ // 开启请求缓存
+ if (isset($option['cache']) && $this->request->isGet()) {
+ $this->parseRequestCache($option['cache']);
+ }
+
+ if (!empty($option['append'])) {
+ $this->request->setRouteVars($option['append']);
+ }
+ }
+
+ /**
+ * 执行路由调度
+ * @access public
+ * @return mixed
+ */
+ public function run()
+ {
+ $option = $this->rule->getOption();
+
+ // 检测路由after行为
+ if (!empty($option['after'])) {
+ $dispatch = $this->checkAfter($option['after']);
+
+ if ($dispatch instanceof Response) {
+ return $dispatch;
+ }
+ }
+
+ // 数据自动验证
+ if (isset($option['validate'])) {
+ $this->autoValidate($option['validate']);
+ }
+
+ $data = $this->exec();
+
+ return $this->autoResponse($data);
+ }
+
+ protected function autoResponse($data)
+ {
+ if ($data instanceof Response) {
+ $response = $data;
+ } elseif (!is_null($data)) {
+ // 默认自动识别响应输出类型
+ $isAjax = $this->request->isAjax();
+ $type = $isAjax ? $this->rule->getConfig('default_ajax_return') : $this->rule->getConfig('default_return_type');
+
+ $response = Response::create($data, $type);
+ } else {
+ $data = ob_get_clean();
+ $content = false === $data ? '' : $data;
+ $status = false === $data ? 204 : 200;
+ $response = Response::create($content, '', $status);
+ }
+
+ return $response;
+ }
+
+ /**
+ * 检查路由后置行为
+ * @access protected
+ * @param mixed $after 后置行为
+ * @return mixed
+ */
+ protected function checkAfter($after)
+ {
+ $this->app['log']->notice('路由后置行为建议使用中间件替代!');
+
+ $hook = $this->app['hook'];
+
+ $result = null;
+
+ foreach ((array) $after as $behavior) {
+ $result = $hook->exec($behavior);
+
+ if (!is_null($result)) {
+ break;
+ }
+ }
+
+ // 路由规则重定向
+ if ($result instanceof Response) {
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * 验证数据
+ * @access protected
+ * @param array $option
+ * @return void
+ * @throws ValidateException
+ */
+ protected function autoValidate($option)
+ {
+ list($validate, $scene, $message, $batch) = $option;
+
+ if (is_array($validate)) {
+ // 指定验证规则
+ $v = $this->app->validate();
+ $v->rule($validate);
+ } else {
+ // 调用验证器
+ $v = $this->app->validate($validate);
+ if (!empty($scene)) {
+ $v->scene($scene);
+ }
+ }
+
+ if (!empty($message)) {
+ $v->message($message);
+ }
+
+ // 批量验证
+ if ($batch) {
+ $v->batch(true);
+ }
+
+ if (!$v->check($this->request->param())) {
+ throw new ValidateException($v->getError());
+ }
+ }
+
+ /**
+ * 处理路由请求缓存
+ * @access protected
+ * @param Request $request 请求对象
+ * @param string|array $cache 路由缓存
+ * @return void
+ */
+ protected function parseRequestCache($cache)
+ {
+ if (is_array($cache)) {
+ list($key, $expire, $tag) = array_pad($cache, 3, null);
+ } else {
+ $key = str_replace('|', '/', $this->request->url());
+ $expire = $cache;
+ $tag = null;
+ }
+
+ $this->request->cache($key, $expire, $tag);
+ }
+
+ /**
+ * 路由绑定模型实例
+ * @access protected
+ * @param array|\Clousre $bindModel 绑定模型
+ * @param array $matches 路由变量
+ * @return void
+ */
+ protected function createBindModel($bindModel, $matches)
+ {
+ foreach ($bindModel as $key => $val) {
+ if ($val instanceof \Closure) {
+ $result = $this->app->invokeFunction($val, $matches);
+ } else {
+ $fields = explode('&', $key);
+
+ if (is_array($val)) {
+ list($model, $exception) = $val;
+ } else {
+ $model = $val;
+ $exception = true;
+ }
+
+ $where = [];
+ $match = true;
+
+ foreach ($fields as $field) {
+ if (!isset($matches[$field])) {
+ $match = false;
+ break;
+ } else {
+ $where[] = [$field, '=', $matches[$field]];
+ }
+ }
+
+ if ($match) {
+ $query = strpos($model, '\\') ? $model::where($where) : $this->app->model($model)->where($where);
+ $result = $query->failException($exception)->find();
+ }
+ }
+
+ if (!empty($result)) {
+ // 注入容器
+ $this->app->instance(get_class($result), $result);
+ }
+ }
+ }
+
+ public function convert($convert)
+ {
+ $this->convert = $convert;
+
+ return $this;
+ }
+
+ public function getDispatch()
+ {
+ return $this->dispatch;
+ }
+
+ public function getParam()
+ {
+ return $this->param;
+ }
+
+ abstract public function exec();
+
+ public function __sleep()
+ {
+ return ['rule', 'dispatch', 'convert', 'param', 'code', 'controller', 'actionName'];
+ }
+
+ public function __wakeup()
+ {
+ $this->app = Container::get('app');
+ $this->request = $this->app['request'];
+ }
+}
diff --git a/thinkphp/library/think/route/Domain.php b/thinkphp/library/think/route/Domain.php
new file mode 100644
index 00000000..260ca500
--- /dev/null
+++ b/thinkphp/library/think/route/Domain.php
@@ -0,0 +1,235 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Container;
+use think\Loader;
+use think\Route;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+use think\route\dispatch\Module as ModuleDispatch;
+
+class Domain extends RuleGroup
+{
+ protected $bind;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param string $name 路由域名
+ * @param mixed $rule 域名路由
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ */
+ public function __construct(Route $router, $name = '', $rule = null, $option = [], $pattern = [])
+ {
+ $this->router = $router;
+ $this->domain = $name;
+ $this->option = $option;
+ $this->rule = $rule;
+ $this->pattern = $pattern;
+ }
+
+ /**
+ * 检测域名路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check($request, $url, $completeMatch = false)
+ {
+ // 检测别名路由
+ $result = $this->checkRouteAlias($request, $url);
+
+ if (false !== $result) {
+ return $result;
+ }
+
+ // 检测URL绑定
+ $result = $this->checkUrlBind($request, $url);
+
+ if (!empty($this->option['append'])) {
+ $request->setRouteVars($this->option['append']);
+ unset($this->option['append']);
+ }
+
+ if (false !== $result) {
+ return $result;
+ }
+
+ // 添加域名中间件
+ if (!empty($this->option['middleware'])) {
+ Container::get('middleware')->import($this->option['middleware']);
+ unset($this->option['middleware']);
+ }
+
+ return parent::check($request, $url, $completeMatch);
+ }
+
+ /**
+ * 设置路由绑定
+ * @access public
+ * @param string $bind 绑定信息
+ * @return $this
+ */
+ public function bind($bind)
+ {
+ $this->bind = $bind;
+ $this->router->bind($bind, $this->domain);
+
+ return $this;
+ }
+
+ /**
+ * 检测路由别名
+ * @access private
+ * @param Request $request
+ * @param string $url URL地址
+ * @return Dispatch|false
+ */
+ private function checkRouteAlias($request, $url)
+ {
+ $alias = strpos($url, '|') ? strstr($url, '|', true) : $url;
+
+ $item = $this->router->getAlias($alias);
+
+ return $item ? $item->check($request, $url) : false;
+ }
+
+ /**
+ * 检测URL绑定
+ * @access private
+ * @param Request $request
+ * @param string $url URL地址
+ * @return Dispatch|false
+ */
+ private function checkUrlBind($request, $url)
+ {
+ if (!empty($this->bind)) {
+ $bind = $this->bind;
+ $this->parseBindAppendParam($bind);
+
+ // 记录绑定信息
+ Container::get('app')->log('[ BIND ] ' . var_export($bind, true));
+
+ // 如果有URL绑定 则进行绑定检测
+ $type = substr($bind, 0, 1);
+ $bind = substr($bind, 1);
+
+ $bindTo = [
+ '\\' => 'bindToClass',
+ '@' => 'bindToController',
+ ':' => 'bindToNamespace',
+ ];
+
+ if (isset($bindTo[$type])) {
+ return $this->{$bindTo[$type]}($request, $url, $bind);
+ }
+ }
+
+ return false;
+ }
+
+ protected function parseBindAppendParam(&$bind)
+ {
+ if (false !== strpos($bind, '?')) {
+ list($bind, $query) = explode('?', $bind);
+ parse_str($query, $vars);
+ $this->append($vars);
+ }
+ }
+
+ /**
+ * 绑定到类
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $class 类名(带命名空间)
+ * @return CallbackDispatch
+ */
+ protected function bindToClass($request, $url, $class)
+ {
+ $array = explode('|', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action');
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($request, $array[1]);
+ }
+
+ return new CallbackDispatch($request, $this, [$class, $action]);
+ }
+
+ /**
+ * 绑定到命名空间
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $namespace 命名空间
+ * @return CallbackDispatch
+ */
+ protected function bindToNamespace($request, $url, $namespace)
+ {
+ $array = explode('|', $url, 3);
+ $class = !empty($array[0]) ? $array[0] : $this->router->config('default_controller');
+ $method = !empty($array[1]) ? $array[1] : $this->router->config('default_action');
+
+ if (!empty($array[2])) {
+ $this->parseUrlParams($request, $array[2]);
+ }
+
+ return new CallbackDispatch($request, $this, [$namespace . '\\' . Loader::parseName($class, 1), $method]);
+ }
+
+ /**
+ * 绑定到控制器类
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $controller 控制器名 (支持带模块名 index/user )
+ * @return ControllerDispatch
+ */
+ protected function bindToController($request, $url, $controller)
+ {
+ $array = explode('|', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action');
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($request, $array[1]);
+ }
+
+ return new ControllerDispatch($request, $this, $controller . '/' . $action);
+ }
+
+ /**
+ * 绑定到模块/控制器
+ * @access protected
+ * @param Request $request
+ * @param string $url URL地址
+ * @param string $controller 控制器类名(带命名空间)
+ * @return ModuleDispatch
+ */
+ protected function bindToModule($request, $url, $controller)
+ {
+ $array = explode('|', $url, 2);
+ $action = !empty($array[0]) ? $array[0] : $this->router->config('default_action');
+
+ if (!empty($array[1])) {
+ $this->parseUrlParams($request, $array[1]);
+ }
+
+ return new ModuleDispatch($request, $this, $controller . '/' . $action);
+ }
+
+}
diff --git a/thinkphp/library/think/route/Resource.php b/thinkphp/library/think/route/Resource.php
new file mode 100644
index 00000000..4e66d9fd
--- /dev/null
+++ b/thinkphp/library/think/route/Resource.php
@@ -0,0 +1,121 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Route;
+
+class Resource extends RuleGroup
+{
+ // 资源路由名称
+ protected $resource;
+
+ // REST路由方法定义
+ protected $rest = [];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 资源名称
+ * @param string $route 路由地址
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ * @param array $rest 资源定义
+ */
+ public function __construct(Route $router, RuleGroup $parent = null, $name = '', $route = '', $option = [], $pattern = [], $rest = [])
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->resource = $name;
+ $this->route = $route;
+ $this->name = strpos($name, '.') ? strstr($name, '.', true) : $name;
+
+ $this->setFullName();
+
+ // 资源路由默认为完整匹配
+ $option['complete_match'] = true;
+
+ $this->pattern = $pattern;
+ $this->option = $option;
+ $this->rest = $rest;
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+ }
+
+ /**
+ * 生成资源路由规则
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $option 路由参数
+ * @return void
+ */
+ protected function buildResourceRule($rule, $option = [])
+ {
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if (strpos($rule, '.')) {
+ // 注册嵌套资源路由
+ $array = explode('.', $rule);
+ $last = array_pop($array);
+ $item = [];
+
+ foreach ($array as $val) {
+ $item[] = $val . '/<' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id') . '>';
+ }
+
+ $rule = implode('/', $item) . '/' . $last;
+ }
+
+ $prefix = substr($rule, strlen($this->name) + 1);
+
+ // 注册资源路由
+ foreach ($this->rest as $key => $val) {
+ if ((isset($option['only']) && !in_array($key, $option['only']))
+ || (isset($option['except']) && in_array($key, $option['except']))) {
+ continue;
+ }
+
+ if (isset($last) && strpos($val[1], '') && isset($option['var'][$last])) {
+ $val[1] = str_replace('', '<' . $option['var'][$last] . '>', $val[1]);
+ } elseif (strpos($val[1], '') && isset($option['var'][$rule])) {
+ $val[1] = str_replace('', '<' . $option['var'][$rule] . '>', $val[1]);
+ }
+
+ $this->addRule(trim($prefix . $val[1], '/'), $this->route . '/' . $val[2], $val[0]);
+ }
+
+ $this->router->setGroup($origin);
+ }
+
+ /**
+ * rest方法定义和修改
+ * @access public
+ * @param string $name 方法名称
+ * @param array|bool $resource 资源
+ * @return $this
+ */
+ public function rest($name, $resource = [])
+ {
+ if (is_array($name)) {
+ $this->rest = $resource ? $name : array_merge($this->rest, $name);
+ } else {
+ $this->rest[$name] = $resource;
+ }
+
+ return $this;
+ }
+}
diff --git a/thinkphp/library/think/route/Rule.php b/thinkphp/library/think/route/Rule.php
new file mode 100644
index 00000000..5cdf42c8
--- /dev/null
+++ b/thinkphp/library/think/route/Rule.php
@@ -0,0 +1,1101 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Container;
+use think\Request;
+use think\Response;
+use think\route\dispatch\Callback as CallbackDispatch;
+use think\route\dispatch\Controller as ControllerDispatch;
+use think\route\dispatch\Module as ModuleDispatch;
+use think\route\dispatch\Redirect as RedirectDispatch;
+use think\route\dispatch\Response as ResponseDispatch;
+use think\route\dispatch\View as ViewDispatch;
+
+abstract class Rule
+{
+ /**
+ * 路由标识
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * 路由对象
+ * @var Route
+ */
+ protected $router;
+
+ /**
+ * 路由所属分组
+ * @var RuleGroup
+ */
+ protected $parent;
+
+ /**
+ * 路由规则
+ * @var mixed
+ */
+ protected $rule;
+
+ /**
+ * 路由地址
+ * @var string|\Closure
+ */
+ protected $route;
+
+ /**
+ * 请求类型
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * 路由变量
+ * @var array
+ */
+ protected $vars = [];
+
+ /**
+ * 路由参数
+ * @var array
+ */
+ protected $option = [];
+
+ /**
+ * 路由变量规则
+ * @var array
+ */
+ protected $pattern = [];
+
+ /**
+ * 需要和分组合并的路由参数
+ * @var array
+ */
+ protected $mergeOptions = ['after', 'before', 'model', 'header', 'response', 'append', 'middleware'];
+
+ /**
+ * 是否需要后置操作
+ * @var bool
+ */
+ protected $doAfter;
+
+ abstract public function check($request, $url, $completeMatch = false);
+
+ /**
+ * 获取Name
+ * @access public
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * 获取当前路由规则
+ * @access public
+ * @return string
+ */
+ public function getRule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 获取当前路由地址
+ * @access public
+ * @return mixed
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
+
+ /**
+ * 获取当前路由的请求类型
+ * @access public
+ * @return string
+ */
+ public function getMethod()
+ {
+ return strtolower($this->method);
+ }
+
+ /**
+ * 获取当前路由的变量
+ * @access public
+ * @return array
+ */
+ public function getVars()
+ {
+ return $this->vars;
+ }
+
+ /**
+ * 获取路由对象
+ * @access public
+ * @return Route
+ */
+ public function getRouter()
+ {
+ return $this->router;
+ }
+
+ /**
+ * 路由是否有后置操作
+ * @access public
+ * @return bool
+ */
+ public function doAfter()
+ {
+ return $this->doAfter;
+ }
+
+ /**
+ * 获取路由分组
+ * @access public
+ * @return RuleGroup|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * 获取路由所在域名
+ * @access public
+ * @return string
+ */
+ public function getDomain()
+ {
+ return $this->parent->getDomain();
+ }
+
+ /**
+ * 获取变量规则定义
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function getPattern($name = '')
+ {
+ if ('' === $name) {
+ return $this->pattern;
+ }
+
+ return isset($this->pattern[$name]) ? $this->pattern[$name] : null;
+ }
+
+ /**
+ * 获取路由参数
+ * @access public
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function getConfig($name = '')
+ {
+ return $this->router->config($name);
+ }
+
+ /**
+ * 获取路由参数定义
+ * @access public
+ * @param string $name 参数名
+ * @return mixed
+ */
+ public function getOption($name = '')
+ {
+ if ('' === $name) {
+ return $this->option;
+ }
+
+ return isset($this->option[$name]) ? $this->option[$name] : null;
+ }
+
+ /**
+ * 注册路由参数
+ * @access public
+ * @param string|array $name 参数名
+ * @param mixed $value 值
+ * @return $this
+ */
+ public function option($name, $value = '')
+ {
+ if (is_array($name)) {
+ $this->option = array_merge($this->option, $name);
+ } else {
+ $this->option[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 注册变量规则
+ * @access public
+ * @param string|array $name 变量名
+ * @param string $rule 变量规则
+ * @return $this
+ */
+ public function pattern($name, $rule = '')
+ {
+ if (is_array($name)) {
+ $this->pattern = array_merge($this->pattern, $name);
+ } else {
+ $this->pattern[$name] = $rule;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置标识
+ * @access public
+ * @param string $name 标识名
+ * @return $this
+ */
+ public function name($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * 设置变量
+ * @access public
+ * @param array $vars 变量
+ * @return $this
+ */
+ public function vars($vars)
+ {
+ $this->vars = $vars;
+
+ return $this;
+ }
+
+ /**
+ * 设置路由请求类型
+ * @access public
+ * @param string $method
+ * @return $this
+ */
+ public function method($method)
+ {
+ return $this->option('method', strtolower($method));
+ }
+
+ /**
+ * 设置路由前置行为
+ * @access public
+ * @param array|\Closure $before
+ * @return $this
+ */
+ public function before($before)
+ {
+ return $this->option('before', $before);
+ }
+
+ /**
+ * 设置路由后置行为
+ * @access public
+ * @param array|\Closure $after
+ * @return $this
+ */
+ public function after($after)
+ {
+ return $this->option('after', $after);
+ }
+
+ /**
+ * 检查后缀
+ * @access public
+ * @param string $ext
+ * @return $this
+ */
+ public function ext($ext = '')
+ {
+ return $this->option('ext', $ext);
+ }
+
+ /**
+ * 检查禁止后缀
+ * @access public
+ * @param string $ext
+ * @return $this
+ */
+ public function denyExt($ext = '')
+ {
+ return $this->option('deny_ext', $ext);
+ }
+
+ /**
+ * 检查域名
+ * @access public
+ * @param string $domain
+ * @return $this
+ */
+ public function domain($domain)
+ {
+ return $this->option('domain', $domain);
+ }
+
+ /**
+ * 设置参数过滤检查
+ * @access public
+ * @param string|array $name
+ * @param mixed $value
+ * @return $this
+ */
+ public function filter($name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->option['filter'] = $name;
+ } else {
+ $this->option['filter'][$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定模型
+ * @access public
+ * @param array|string $var 路由变量名 多个使用 & 分割
+ * @param string|\Closure $model 绑定模型类
+ * @param bool $exception 是否抛出异常
+ * @return $this
+ */
+ public function model($var, $model = null, $exception = true)
+ {
+ if ($var instanceof \Closure) {
+ $this->option['model'][] = $var;
+ } elseif (is_array($var)) {
+ $this->option['model'] = $var;
+ } elseif (is_null($model)) {
+ $this->option['model']['id'] = [$var, true];
+ } else {
+ $this->option['model'][$var] = [$model, $exception];
+ }
+
+ return $this;
+ }
+
+ /**
+ * 附加路由隐式参数
+ * @access public
+ * @param array $append
+ * @return $this
+ */
+ public function append(array $append = [])
+ {
+ if (isset($this->option['append'])) {
+ $this->option['append'] = array_merge($this->option['append'], $append);
+ } else {
+ $this->option['append'] = $append;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 绑定验证
+ * @access public
+ * @param mixed $validate 验证器类
+ * @param string $scene 验证场景
+ * @param array $message 验证提示
+ * @param bool $batch 批量验证
+ * @return $this
+ */
+ public function validate($validate, $scene = null, $message = [], $batch = false)
+ {
+ $this->option['validate'] = [$validate, $scene, $message, $batch];
+
+ return $this;
+ }
+
+ /**
+ * 绑定Response对象
+ * @access public
+ * @param mixed $response
+ * @return $this
+ */
+ public function response($response)
+ {
+ $this->option['response'][] = $response;
+ return $this;
+ }
+
+ /**
+ * 设置Response Header信息
+ * @access public
+ * @param string|array $name 参数名
+ * @param string $value 参数值
+ * @return $this
+ */
+ public function header($header, $value = null)
+ {
+ if (is_array($header)) {
+ $this->option['header'] = $header;
+ } else {
+ $this->option['header'][$header] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 指定路由中间件
+ * @access public
+ * @param string|array|\Closure $middleware
+ * @param mixed $param
+ * @return $this
+ */
+ public function middleware($middleware, $param = null)
+ {
+ if (is_null($param) && is_array($middleware)) {
+ $this->option['middleware'] = $middleware;
+ } else {
+ foreach ((array) $middleware as $item) {
+ $this->option['middleware'][] = [$item, $param];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置路由缓存
+ * @access public
+ * @param array|string $cache
+ * @return $this
+ */
+ public function cache($cache)
+ {
+ return $this->option('cache', $cache);
+ }
+
+ /**
+ * 检查URL分隔符
+ * @access public
+ * @param bool $depr
+ * @return $this
+ */
+ public function depr($depr)
+ {
+ return $this->option('param_depr', $depr);
+ }
+
+ /**
+ * 是否合并额外参数
+ * @access public
+ * @param bool $merge
+ * @return $this
+ */
+ public function mergeExtraVars($merge = true)
+ {
+ return $this->option('merge_extra_vars', $merge);
+ }
+
+ /**
+ * 设置需要合并的路由参数
+ * @access public
+ * @param array $option
+ * @return $this
+ */
+ public function mergeOptions($option = [])
+ {
+ $this->mergeOptions = array_merge($this->mergeOptions, $option);
+ return $this;
+ }
+
+ /**
+ * 检查是否为HTTPS请求
+ * @access public
+ * @param bool $https
+ * @return $this
+ */
+ public function https($https = true)
+ {
+ return $this->option('https', $https);
+ }
+
+ /**
+ * 检查是否为AJAX请求
+ * @access public
+ * @param bool $ajax
+ * @return $this
+ */
+ public function ajax($ajax = true)
+ {
+ return $this->option('ajax', $ajax);
+ }
+
+ /**
+ * 检查是否为PJAX请求
+ * @access public
+ * @param bool $pjax
+ * @return $this
+ */
+ public function pjax($pjax = true)
+ {
+ return $this->option('pjax', $pjax);
+ }
+
+ /**
+ * 检查是否为手机访问
+ * @access public
+ * @param bool $mobile
+ * @return $this
+ */
+ public function mobile($mobile = true)
+ {
+ return $this->option('mobile', $mobile);
+ }
+
+ /**
+ * 当前路由到一个模板地址 当使用数组的时候可以传入模板变量
+ * @access public
+ * @param bool|array $view
+ * @return $this
+ */
+ public function view($view = true)
+ {
+ return $this->option('view', $view);
+ }
+
+ /**
+ * 当前路由为重定向
+ * @access public
+ * @param bool $redirect 是否为重定向
+ * @return $this
+ */
+ public function redirect($redirect = true)
+ {
+ return $this->option('redirect', $redirect);
+ }
+
+ /**
+ * 设置路由完整匹配
+ * @access public
+ * @param bool $match
+ * @return $this
+ */
+ public function completeMatch($match = true)
+ {
+ return $this->option('complete_match', $match);
+ }
+
+ /**
+ * 是否去除URL最后的斜线
+ * @access public
+ * @param bool $remove
+ * @return $this
+ */
+ public function removeSlash($remove = true)
+ {
+ return $this->option('remove_slash', $remove);
+ }
+
+ /**
+ * 设置是否允许跨域
+ * @access public
+ * @param bool $allow
+ * @param array $header
+ * @return $this
+ */
+ public function allowCrossDomain($allow = true, $header = [])
+ {
+ if (!empty($header)) {
+ $this->header($header);
+ }
+
+ if ($allow && $this->parent) {
+ $this->parent->addRuleItem($this, 'options');
+ }
+
+ return $this->option('cross_domain', $allow);
+ }
+
+ /**
+ * 检查OPTIONS请求
+ * @access public
+ * @param Request $request
+ * @return Dispatch|void
+ */
+ protected function checkCrossDomain($request)
+ {
+ if (!empty($this->option['cross_domain'])) {
+
+ $header = [
+ 'Access-Control-Allow-Origin' => '*',
+ 'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE',
+ 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With',
+ ];
+
+ if (!empty($this->option['header'])) {
+ $header = array_merge($header, $this->option['header']);
+ }
+
+ $this->option['header'] = $header;
+
+ if ($request->method(true) == 'OPTIONS') {
+ return new ResponseDispatch($request, $this, Response::create()->code(204)->header($header));
+ }
+ }
+ }
+
+ /**
+ * 设置路由规则全局有效
+ * @access public
+ * @return $this
+ */
+ public function crossDomainRule()
+ {
+ if ($this instanceof RuleGroup) {
+ $method = '*';
+ } else {
+ $method = $this->method;
+ }
+
+ $this->router->setCrossDomainRule($this, $method);
+
+ return $this;
+ }
+
+ /**
+ * 合并分组参数
+ * @access protected
+ * @return void
+ */
+ protected function mergeGroupOptions()
+ {
+ $parentOption = $this->parent->getOption();
+ // 合并分组参数
+ foreach ($this->mergeOptions as $item) {
+ if (isset($parentOption[$item]) && isset($this->option[$item])) {
+ $this->option[$item] = array_merge($parentOption[$item], $this->option[$item]);
+ }
+ }
+
+ $this->option = array_merge($parentOption, $this->option);
+
+ return $this->option;
+ }
+
+ /**
+ * 解析匹配到的规则路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param array $matches 匹配的变量
+ * @return Dispatch
+ */
+ public function parseRule($request, $rule, $route, $url, $option = [], $matches = [])
+ {
+ if (is_string($route) && isset($option['prefix'])) {
+ // 路由地址前缀
+ $route = $option['prefix'] . $route;
+ }
+
+ // 替换路由地址中的变量
+ if (is_string($route) && !empty($matches)) {
+ foreach ($matches as $key => $val) {
+ if (false !== strpos($route, '<' . $key . '>')) {
+ $route = str_replace('<' . $key . '>', $val, $route);
+ } elseif (false !== strpos($route, ':' . $key)) {
+ $route = str_replace(':' . $key, $val, $route);
+ }
+ }
+ }
+
+ // 解析额外参数
+ $count = substr_count($rule, '/');
+ $url = array_slice(explode('|', $url), $count + 1);
+ $this->parseUrlParams($request, implode('|', $url), $matches);
+
+ $this->route = $route;
+ $this->vars = $matches;
+ $this->option = $option;
+ $this->doAfter = true;
+
+ // 发起路由调度
+ return $this->dispatch($request, $route, $option);
+ }
+
+ /**
+ * 检查路由前置行为
+ * @access protected
+ * @param mixed $before 前置行为
+ * @return mixed
+ */
+ protected function checkBefore($before)
+ {
+ $hook = Container::get('hook');
+
+ foreach ((array) $before as $behavior) {
+ $result = $hook->exec($behavior);
+
+ if (false === $result) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 发起路由调度
+ * @access protected
+ * @param Request $request Request对象
+ * @param mixed $route 路由地址
+ * @param array $option 路由参数
+ * @return Dispatch
+ */
+ protected function dispatch($request, $route, $option)
+ {
+ if ($route instanceof \Closure) {
+ // 执行闭包
+ $result = new CallbackDispatch($request, $this, $route);
+ } elseif ($route instanceof Response) {
+ $result = new ResponseDispatch($request, $this, $route);
+ } elseif (isset($option['view']) && false !== $option['view']) {
+ $result = new ViewDispatch($request, $this, $route, is_array($option['view']) ? $option['view'] : []);
+ } elseif (!empty($option['redirect']) || 0 === strpos($route, '/') || strpos($route, '://')) {
+ // 路由到重定向地址
+ $result = new RedirectDispatch($request, $this, $route, [], isset($option['status']) ? $option['status'] : 301);
+ } elseif (false !== strpos($route, '\\')) {
+ // 路由到方法
+ $result = $this->dispatchMethod($request, $route);
+ } elseif (0 === strpos($route, '@')) {
+ // 路由到控制器
+ $result = $this->dispatchController($request, substr($route, 1));
+ } else {
+ // 路由到模块/控制器/操作
+ $result = $this->dispatchModule($request, $route);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析URL地址为 模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return CallbackDispatch
+ */
+ protected function dispatchMethod($request, $route)
+ {
+ list($path, $var) = $this->parseUrlPath($route);
+
+ $route = str_replace('/', '@', implode('/', $path));
+ $method = strpos($route, '@') ? explode('@', $route) : $route;
+
+ return new CallbackDispatch($request, $this, $method, $var);
+ }
+
+ /**
+ * 解析URL地址为 模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return ControllerDispatch
+ */
+ protected function dispatchController($request, $route)
+ {
+ list($route, $var) = $this->parseUrlPath($route);
+
+ $result = new ControllerDispatch($request, $this, implode('/', $route), $var);
+
+ $request->setAction(array_pop($route));
+ $request->setController($route ? array_pop($route) : $this->getConfig('default_controller'));
+ $request->setModule($route ? array_pop($route) : $this->getConfig('default_module'));
+
+ return $result;
+ }
+
+ /**
+ * 解析URL地址为 模块/控制器/操作
+ * @access protected
+ * @param Request $request Request对象
+ * @param string $route 路由地址
+ * @return ModuleDispatch
+ */
+ protected function dispatchModule($request, $route)
+ {
+ list($path, $var) = $this->parseUrlPath($route);
+
+ $action = array_pop($path);
+ $controller = !empty($path) ? array_pop($path) : null;
+ $module = $this->getConfig('app_multi_module') && !empty($path) ? array_pop($path) : null;
+ $method = $request->method();
+
+ if ($this->getConfig('use_action_prefix') && $this->router->getMethodPrefix($method)) {
+ $prefix = $this->router->getMethodPrefix($method);
+ // 操作方法前缀支持
+ $action = 0 !== strpos($action, $prefix) ? $prefix . $action : $action;
+ }
+
+ // 设置当前请求的路由变量
+ $request->setRouteVars($var);
+
+ // 路由到模块/控制器/操作
+ return new ModuleDispatch($request, $this, [$module, $controller, $action], ['convert' => false]);
+ }
+
+ /**
+ * 路由检查
+ * @access protected
+ * @param array $option 路由参数
+ * @param Request $request Request对象
+ * @return bool
+ */
+ protected function checkOption($option, Request $request)
+ {
+ // 请求类型检测
+ if (!empty($option['method'])) {
+ if (is_string($option['method']) && false === stripos($option['method'], $request->method())) {
+ return false;
+ }
+ }
+
+ // AJAX PJAX 请求检查
+ foreach (['ajax', 'pjax', 'mobile'] as $item) {
+ if (isset($option[$item])) {
+ $call = 'is' . $item;
+ if ($option[$item] && !$request->$call() || !$option[$item] && $request->$call()) {
+ return false;
+ }
+ }
+ }
+
+ // 伪静态后缀检测
+ if ($request->url() != '/' && ((isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|'))
+ || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|')))) {
+ return false;
+ }
+
+ // 域名检查
+ if ((isset($option['domain']) && !in_array($option['domain'], [$request->host(true), $request->subDomain()]))) {
+ return false;
+ }
+
+ // HTTPS检查
+ if ((isset($option['https']) && $option['https'] && !$request->isSsl())
+ || (isset($option['https']) && !$option['https'] && $request->isSsl())) {
+ return false;
+ }
+
+ // 请求参数检查
+ if (isset($option['filter'])) {
+ foreach ($option['filter'] as $name => $value) {
+ if ($request->param($name, '', null) != $value) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 解析URL地址中的参数Request对象
+ * @access protected
+ * @param Request $request
+ * @param string $rule 路由规则
+ * @param array $var 变量
+ * @return void
+ */
+ protected function parseUrlParams($request, $url, &$var = [])
+ {
+ if ($url) {
+ if ($this->getConfig('url_param_type')) {
+ $var += explode('|', $url);
+ } else {
+ preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
+ $var[$match[1]] = strip_tags($match[2]);
+ }, $url);
+ }
+ }
+ }
+
+ /**
+ * 解析URL的pathinfo参数和变量
+ * @access public
+ * @param string $url URL地址
+ * @return array
+ */
+ public function parseUrlPath($url)
+ {
+ // 分隔符替换 确保路由定义使用统一的分隔符
+ $url = str_replace('|', '/', $url);
+ $url = trim($url, '/');
+ $var = [];
+
+ if (false !== strpos($url, '?')) {
+ // [模块/控制器/操作?]参数1=值1&参数2=值2...
+ $info = parse_url($url);
+ $path = explode('/', $info['path']);
+ parse_str($info['query'], $var);
+ } elseif (strpos($url, '/')) {
+ // [模块/控制器/操作]
+ $path = explode('/', $url);
+ } elseif (false !== strpos($url, '=')) {
+ // 参数1=值1&参数2=值2...
+ $path = [];
+ parse_str($url, $var);
+ } else {
+ $path = [$url];
+ }
+
+ return [$path, $var];
+ }
+
+ /**
+ * 生成路由的正则规则
+ * @access protected
+ * @param string $rule 路由规则
+ * @param array $match 匹配的变量
+ * @param array $pattern 路由变量规则
+ * @param array $option 路由参数
+ * @param bool $completeMatch 路由是否完全匹配
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildRuleRegex($rule, $match, $pattern = [], $option = [], $completeMatch = false, $suffix = '')
+ {
+ foreach ($match as $name) {
+ $replace[] = $this->buildNameRegex($name, $pattern, $suffix);
+ }
+
+ // 是否区分 / 地址访问
+ if ('/' != $rule) {
+ if (!empty($option['remove_slash'])) {
+ $rule = rtrim($rule, '/');
+ } elseif (substr($rule, -1) == '/') {
+ $rule = rtrim($rule, '/');
+ $hasSlash = true;
+ }
+ }
+
+ $regex = str_replace($match, $replace, $rule);
+ $regex = str_replace([')?/', ')/', ')?-', ')-', '\\\\/'], [')\/', ')\/', ')\-', ')\-', '\/'], $regex);
+
+ if (isset($hasSlash)) {
+ $regex .= '\/';
+ }
+
+ return $regex . ($completeMatch ? '$' : '');
+ }
+
+ /**
+ * 生成路由变量的正则规则
+ * @access protected
+ * @param string $name 路由变量
+ * @param string $pattern 变量规则
+ * @param string $suffix 路由正则变量后缀
+ * @return string
+ */
+ protected function buildNameRegex($name, $pattern, $suffix)
+ {
+ $optional = '';
+ $slash = substr($name, 0, 1);
+
+ if (in_array($slash, ['/', '-'])) {
+ $prefix = '\\' . $slash;
+ $name = substr($name, 1);
+ $slash = substr($name, 0, 1);
+ } else {
+ $prefix = '';
+ }
+
+ if ('<' != $slash) {
+ return $prefix . preg_quote($name, '/');
+ }
+
+ if (strpos($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = '?';
+ } elseif (strpos($name, '>')) {
+ $name = substr($name, 1, -1);
+ }
+
+ if (isset($pattern[$name])) {
+ $nameRule = $pattern[$name];
+ if (0 === strpos($nameRule, '/') && '/' == substr($nameRule, -1)) {
+ $nameRule = substr($nameRule, 1, -1);
+ }
+ } else {
+ $nameRule = $this->getConfig('default_route_pattern');
+ }
+
+ return '(' . $prefix . '(?<' . $name . $suffix . '>' . $nameRule . '))' . $optional;
+ }
+
+ /**
+ * 分析路由规则中的变量
+ * @access protected
+ * @param string $rule 路由规则
+ * @return array
+ */
+ protected function parseVar($rule)
+ {
+ // 提取路由规则中的变量
+ $var = [];
+
+ if (preg_match_all('/<\w+\??>/', $rule, $matches)) {
+ foreach ($matches[0] as $name) {
+ $optional = false;
+
+ if (strpos($name, '?')) {
+ $name = substr($name, 1, -2);
+ $optional = true;
+ } else {
+ $name = substr($name, 1, -1);
+ }
+
+ $var[$name] = $optional ? 2 : 1;
+ }
+ }
+
+ return $var;
+ }
+
+ /**
+ * 设置路由参数
+ * @access public
+ * @param string $method 方法名
+ * @param array $args 调用参数
+ * @return $this
+ */
+ public function __call($method, $args)
+ {
+ if (count($args) > 1) {
+ $args[0] = $args;
+ }
+ array_unshift($args, $method);
+
+ return call_user_func_array([$this, 'option'], $args);
+ }
+
+ public function __sleep()
+ {
+ return ['name', 'rule', 'route', 'method', 'vars', 'option', 'pattern', 'doAfter'];
+ }
+
+ public function __wakeup()
+ {
+ $this->router = Container::get('route');
+ }
+}
diff --git a/thinkphp/library/think/route/RuleGroup.php b/thinkphp/library/think/route/RuleGroup.php
new file mode 100644
index 00000000..576fe91b
--- /dev/null
+++ b/thinkphp/library/think/route/RuleGroup.php
@@ -0,0 +1,575 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Container;
+use think\Exception;
+use think\Request;
+use think\Response;
+use think\Route;
+use think\route\dispatch\Response as ResponseDispatch;
+use think\route\dispatch\Url as UrlDispatch;
+
+class RuleGroup extends Rule
+{
+ // 分组路由(包括子分组)
+ protected $rules = [
+ '*' => [],
+ 'get' => [],
+ 'post' => [],
+ 'put' => [],
+ 'patch' => [],
+ 'delete' => [],
+ 'head' => [],
+ 'options' => [],
+ ];
+
+ // MISS路由
+ protected $miss;
+
+ // 自动路由
+ protected $auto;
+
+ // 完整名称
+ protected $fullName;
+
+ // 所在域名
+ protected $domain;
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由对象
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 分组名称
+ * @param mixed $rule 分组路由
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ */
+ public function __construct(Route $router, RuleGroup $parent = null, $name = '', $rule = [], $option = [], $pattern = [])
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->rule = $rule;
+ $this->name = trim($name, '/');
+ $this->option = $option;
+ $this->pattern = $pattern;
+
+ $this->setFullName();
+
+ if ($this->parent) {
+ $this->domain = $this->parent->getDomain();
+ $this->parent->addRuleItem($this);
+ }
+
+ if (!empty($option['cross_domain'])) {
+ $this->router->setCrossDomainRule($this);
+ }
+ }
+
+ /**
+ * 设置分组的路由规则
+ * @access public
+ * @return void
+ */
+ protected function setFullName()
+ {
+ if (false !== strpos($this->name, ':')) {
+ $this->name = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $this->name);
+ }
+
+ if ($this->parent && $this->parent->getFullName()) {
+ $this->fullName = $this->parent->getFullName() . ($this->name ? '/' . $this->name : '');
+ } else {
+ $this->fullName = $this->name;
+ }
+ }
+
+ /**
+ * 获取所属域名
+ * @access public
+ * @return string
+ */
+ public function getDomain()
+ {
+ return $this->domain;
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check($request, $url, $completeMatch = false)
+ {
+ if ($dispatch = $this->checkCrossDomain($request)) {
+ // 跨域OPTIONS请求
+ return $dispatch;
+ }
+
+ // 检查分组有效性
+ if (!$this->checkOption($this->option, $request) || !$this->checkUrl($url)) {
+ return false;
+ }
+
+ // 解析分组路由
+ if ($this instanceof Resource) {
+ $this->buildResourceRule($this->resource, $this->option);
+ } elseif ($this->rule) {
+ if ($this->rule instanceof Response) {
+ return new ResponseDispatch($request, $this, $this->rule);
+ }
+
+ $this->parseGroupRule($this->rule);
+ }
+
+ // 获取当前路由规则
+ $method = strtolower($request->method());
+ $rules = $this->getMethodRules($method);
+
+ if (count($rules) == 0) {
+ return false;
+ }
+
+ if ($this->parent) {
+ // 合并分组参数
+ $this->mergeGroupOptions();
+ // 合并分组变量规则
+ $this->pattern = array_merge($this->parent->getPattern(), $this->pattern);
+ }
+
+ if (isset($this->option['complete_match'])) {
+ $completeMatch = $this->option['complete_match'];
+ }
+
+ if (!empty($this->option['merge_rule_regex'])) {
+ // 合并路由正则规则进行路由匹配检查
+ $result = $this->checkMergeRuleRegex($request, $rules, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+
+ // 检查分组路由
+ foreach ($rules as $key => $item) {
+ $result = $item->check($request, $url, $completeMatch);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+
+ if ($this->auto) {
+ // 自动解析URL地址
+ $result = new UrlDispatch($request, $this, $this->auto . '/' . $url, ['auto_search' => false]);
+ } elseif ($this->miss && in_array($this->miss->getMethod(), ['*', $method])) {
+ // 未匹配所有路由的路由规则处理
+ $result = $this->miss->parseRule($request, '', $this->miss->getRoute(), $url, $this->miss->getOption());
+ } else {
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取当前请求的路由规则(包括子分组、资源路由)
+ * @access protected
+ * @param string $method
+ * @return array
+ */
+ protected function getMethodRules($method)
+ {
+ return array_merge($this->rules[$method], $this->rules['*']);
+ }
+
+ /**
+ * 分组URL匹配检查
+ * @access protected
+ * @param string $url
+ * @return bool
+ */
+ protected function checkUrl($url)
+ {
+ if ($this->fullName) {
+ $pos = strpos($this->fullName, '<');
+
+ if (false !== $pos) {
+ $str = substr($this->fullName, 0, $pos);
+ } else {
+ $str = $this->fullName;
+ }
+
+ if ($str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 延迟解析分组的路由规则
+ * @access public
+ * @param bool $lazy 路由是否延迟解析
+ * @return $this
+ */
+ public function lazy($lazy = true)
+ {
+ if (!$lazy) {
+ $this->parseGroupRule($this->rule);
+ $this->rule = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * 解析分组和域名的路由规则及绑定
+ * @access public
+ * @param mixed $rule 路由规则
+ * @return void
+ */
+ public function parseGroupRule($rule)
+ {
+ $origin = $this->router->getGroup();
+ $this->router->setGroup($this);
+
+ if ($rule instanceof \Closure) {
+ Container::getInstance()->invokeFunction($rule);
+ } elseif (is_array($rule)) {
+ $this->addRules($rule);
+ } elseif (is_string($rule) && $rule) {
+ $this->router->bind($rule, $this->domain);
+ }
+
+ $this->router->setGroup($origin);
+ }
+
+ /**
+ * 检测分组路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param array $rules 路由规则
+ * @param string $url 访问地址
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ protected function checkMergeRuleRegex($request, &$rules, $url, $completeMatch)
+ {
+ $depr = $this->router->config('pathinfo_depr');
+ $url = $depr . str_replace('|', $depr, $url);
+
+ foreach ($rules as $key => $item) {
+ if ($item instanceof RuleItem) {
+ $rule = $depr . str_replace('/', $depr, $item->getRule());
+ if ($depr == $rule && $depr != $url) {
+ unset($rules[$key]);
+ continue;
+ }
+
+ $complete = null !== $item->getOption('complete_match') ? $item->getOption('complete_match') : $completeMatch;
+
+ if (false === strpos($rule, '<')) {
+ if (0 === strcasecmp($rule, $url) || (!$complete && 0 === strncasecmp($rule, $url, strlen($rule)))) {
+ return $item->checkRule($request, $url, []);
+ }
+
+ unset($rules[$key]);
+ continue;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ unset($rules[$key]);
+ continue;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ unset($rules[$key]);
+ $pattern = array_merge($this->getPattern(), $item->getPattern());
+ $option = array_merge($this->getOption(), $item->getOption());
+
+ $regex[$key] = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $complete, '_THINK_' . $key);
+ $items[$key] = $item;
+ }
+ }
+ }
+
+ if (empty($regex)) {
+ return false;
+ }
+
+ try {
+ $result = preg_match('/^(?:' . implode('|', $regex) . ')/u', $url, $match);
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ if ($result) {
+ $var = [];
+ foreach ($match as $key => $val) {
+ if (is_string($key) && '' !== $val) {
+ list($name, $pos) = explode('_THINK_', $key);
+
+ $var[$name] = $val;
+ }
+ }
+
+ if (!isset($pos)) {
+ foreach ($regex as $key => $item) {
+ if (0 === strpos(str_replace(['\/', '\-', '\\' . $depr], ['/', '-', $depr], $item), $match[0])) {
+ $pos = $key;
+ break;
+ }
+ }
+ }
+
+ $rule = $items[$pos]->getRule();
+ $array = $this->router->getRule($rule);
+
+ foreach ($array as $item) {
+ if (in_array($item->getMethod(), ['*', strtolower($request->method())])) {
+ $result = $item->checkRule($request, $url, $var);
+
+ if (false !== $result) {
+ return $result;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取分组的MISS路由
+ * @access public
+ * @return RuleItem|null
+ */
+ public function getMissRule()
+ {
+ return $this->miss;
+ }
+
+ /**
+ * 获取分组的自动路由
+ * @access public
+ * @return string
+ */
+ public function getAutoRule()
+ {
+ return $this->auto;
+ }
+
+ /**
+ * 注册自动路由
+ * @access public
+ * @param string $route 路由规则
+ * @return void
+ */
+ public function addAutoRule($route)
+ {
+ $this->auto = $route;
+ }
+
+ /**
+ * 注册MISS路由
+ * @access public
+ * @param string $route 路由地址
+ * @param string $method 请求类型
+ * @param array $option 路由参数
+ * @return RuleItem
+ */
+ public function addMissRule($route, $method = '*', $option = [])
+ {
+ // 创建路由规则实例
+ $ruleItem = new RuleItem($this->router, $this, null, '', $route, strtolower($method), $option);
+
+ $this->miss = $ruleItem;
+
+ return $ruleItem;
+ }
+
+ /**
+ * 添加分组下的路由规则或者子分组
+ * @access public
+ * @param string $rule 路由规则
+ * @param string $route 路由地址
+ * @param string $method 请求类型
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ * @return $this
+ */
+ public function addRule($rule, $route, $method = '*', $option = [], $pattern = [])
+ {
+ // 读取路由标识
+ if (is_array($rule)) {
+ $name = $rule[0];
+ $rule = $rule[1];
+ } elseif (is_string($route)) {
+ $name = $route;
+ } else {
+ $name = null;
+ }
+
+ $method = strtolower($method);
+
+ if ('/' === $rule || '' === $rule) {
+ // 首页自动完整匹配
+ $rule .= '$';
+ }
+
+ // 创建路由规则实例
+ $ruleItem = new RuleItem($this->router, $this, $name, $rule, $route, $method, $option, $pattern);
+
+ if (!empty($option['cross_domain'])) {
+ $this->router->setCrossDomainRule($ruleItem, $method);
+ }
+
+ $this->addRuleItem($ruleItem, $method);
+
+ return $ruleItem;
+ }
+
+ /**
+ * 批量注册路由规则
+ * @access public
+ * @param array $rules 路由规则
+ * @param string $method 请求类型
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ * @return void
+ */
+ public function addRules($rules, $method = '*', $option = [], $pattern = [])
+ {
+ foreach ($rules as $key => $val) {
+ if (is_numeric($key)) {
+ $key = array_shift($val);
+ }
+
+ if (is_array($val)) {
+ $route = array_shift($val);
+ $option = $val ? array_shift($val) : [];
+ $pattern = $val ? array_shift($val) : [];
+ } else {
+ $route = $val;
+ }
+
+ $this->addRule($key, $route, $method, $option, $pattern);
+ }
+ }
+
+ public function addRuleItem($rule, $method = '*')
+ {
+ if (strpos($method, '|')) {
+ $rule->method($method);
+ $method = '*';
+ }
+
+ $this->rules[$method][] = $rule;
+
+ return $this;
+ }
+
+ /**
+ * 设置分组的路由前缀
+ * @access public
+ * @param string $prefix
+ * @return $this
+ */
+ public function prefix($prefix)
+ {
+ if ($this->parent && $this->parent->getOption('prefix')) {
+ $prefix = $this->parent->getOption('prefix') . $prefix;
+ }
+
+ return $this->option('prefix', $prefix);
+ }
+
+ /**
+ * 设置资源允许
+ * @access public
+ * @param array $only
+ * @return $this
+ */
+ public function only($only)
+ {
+ return $this->option('only', $only);
+ }
+
+ /**
+ * 设置资源排除
+ * @access public
+ * @param array $except
+ * @return $this
+ */
+ public function except($except)
+ {
+ return $this->option('except', $except);
+ }
+
+ /**
+ * 设置资源路由的变量
+ * @access public
+ * @param array $vars
+ * @return $this
+ */
+ public function vars($vars)
+ {
+ return $this->option('var', $vars);
+ }
+
+ /**
+ * 合并分组的路由规则正则
+ * @access public
+ * @param bool $merge
+ * @return $this
+ */
+ public function mergeRuleRegex($merge = true)
+ {
+ return $this->option('merge_rule_regex', $merge);
+ }
+
+ /**
+ * 获取完整分组Name
+ * @access public
+ * @return string
+ */
+ public function getFullName()
+ {
+ return $this->fullName;
+ }
+
+ /**
+ * 获取分组的路由规则
+ * @access public
+ * @param string $method
+ * @return array
+ */
+ public function getRules($method = '')
+ {
+ if ('' === $method) {
+ return $this->rules;
+ }
+
+ return isset($this->rules[strtolower($method)]) ? $this->rules[strtolower($method)] : [];
+ }
+
+}
diff --git a/thinkphp/library/think/route/RuleItem.php b/thinkphp/library/think/route/RuleItem.php
new file mode 100644
index 00000000..91c97ad4
--- /dev/null
+++ b/thinkphp/library/think/route/RuleItem.php
@@ -0,0 +1,282 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+use think\Container;
+use think\Exception;
+use think\Route;
+
+class RuleItem extends Rule
+{
+ /**
+ * 架构函数
+ * @access public
+ * @param Route $router 路由实例
+ * @param RuleGroup $parent 上级对象
+ * @param string $name 路由标识
+ * @param string|array $rule 路由规则
+ * @param string|\Closure $route 路由地址
+ * @param string $method 请求类型
+ * @param array $option 路由参数
+ * @param array $pattern 变量规则
+ */
+ public function __construct(Route $router, RuleGroup $parent, $name, $rule, $route, $method = '*', $option = [], $pattern = [])
+ {
+ $this->router = $router;
+ $this->parent = $parent;
+ $this->name = $name;
+ $this->route = $route;
+ $this->method = $method;
+ $this->option = $option;
+ $this->pattern = $pattern;
+
+ $this->setRule($rule);
+
+ if (!empty($option['cross_domain'])) {
+ $this->router->setCrossDomainRule($this, $method);
+ }
+ }
+
+ /**
+ * 路由规则预处理
+ * @access public
+ * @param string $rule 路由规则
+ * @return void
+ */
+ public function setRule($rule)
+ {
+ if ('$' == substr($rule, -1, 1)) {
+ // 是否完整匹配
+ $rule = substr($rule, 0, -1);
+
+ $this->option['complete_match'] = true;
+ }
+
+ $rule = '/' != $rule ? ltrim($rule, '/') : '';
+
+ if ($this->parent && $prefix = $this->parent->getFullName()) {
+ $rule = $prefix . ($rule ? '/' . ltrim($rule, '/') : '');
+ }
+
+ if (false !== strpos($rule, ':')) {
+ $this->rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $rule);
+ } else {
+ $this->rule = $rule;
+ }
+
+ // 生成路由标识的快捷访问
+ $this->setRuleName();
+ }
+
+ /**
+ * 检查后缀
+ * @access public
+ * @param string $ext
+ * @return $this
+ */
+ public function ext($ext = '')
+ {
+ $this->option('ext', $ext);
+ $this->setRuleName(true);
+
+ return $this;
+ }
+
+ /**
+ * 设置别名
+ * @access public
+ * @param string $name
+ * @return $this
+ */
+ public function name($name)
+ {
+ $this->name = $name;
+ $this->setRuleName(true);
+
+ return $this;
+ }
+
+ /**
+ * 设置路由标识 用于URL反解生成
+ * @access protected
+ * @param bool $first 是否插入开头
+ * @return void
+ */
+ protected function setRuleName($first = false)
+ {
+ if ($this->name) {
+ $vars = $this->parseVar($this->rule);
+ $name = strtolower($this->name);
+
+ if (isset($this->option['ext'])) {
+ $suffix = $this->option['ext'];
+ } elseif ($this->parent->getOption('ext')) {
+ $suffix = $this->parent->getOption('ext');
+ } else {
+ $suffix = null;
+ }
+
+ $value = [$this->rule, $vars, $this->parent->getDomain(), $suffix, $this->method];
+
+ Container::get('rule_name')->set($name, $value, $first);
+ Container::get('rule_name')->setRule($this->rule, $this);
+ }
+ }
+
+ /**
+ * 检测路由
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $match 匹配路由变量
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function checkRule($request, $url, $match = null, $completeMatch = false)
+ {
+ if ($dispatch = $this->checkCrossDomain($request)) {
+ // 允许跨域
+ return $dispatch;
+ }
+
+ // 检查参数有效性
+ if (!$this->checkOption($this->option, $request)) {
+ return false;
+ }
+
+ // 合并分组参数
+ $option = $this->mergeGroupOptions();
+
+ // 检查前置行为
+ if (isset($option['before']) && false === $this->checkBefore($option['before'])) {
+ return false;
+ }
+
+ $url = $this->urlSuffixCheck($request, $url, $option);
+
+ if (is_null($match)) {
+ $match = $this->match($url, $option, $completeMatch);
+ }
+
+ if (false !== $match) {
+ return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match);
+ }
+
+ return false;
+ }
+
+ /**
+ * 检测路由(含路由匹配)
+ * @access public
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param string $depr 路径分隔符
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return Dispatch|false
+ */
+ public function check($request, $url, $completeMatch = false)
+ {
+ return $this->checkRule($request, $url, null, $completeMatch);
+ }
+
+ /**
+ * URL后缀及Slash检查
+ * @access protected
+ * @param Request $request 请求对象
+ * @param string $url 访问地址
+ * @param array $option 路由参数
+ * @return string
+ */
+ protected function urlSuffixCheck($request, $url, $option = [])
+ {
+ // 是否区分 / 地址访问
+ if (!empty($option['remove_slash']) && '/' != $this->rule) {
+ $this->rule = rtrim($this->rule, '/');
+ $url = rtrim($url, '|');
+ }
+
+ if (isset($option['ext'])) {
+ // 路由ext参数 优先于系统配置的URL伪静态后缀参数
+ $url = preg_replace('/\.(' . $request->ext() . ')$/i', '', $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * 检测URL和规则路由是否匹配
+ * @access private
+ * @param string $url URL地址
+ * @param array $option 路由参数
+ * @param bool $completeMatch 路由是否完全匹配
+ * @return array|false
+ */
+ private function match($url, $option, $completeMatch)
+ {
+ if (isset($option['complete_match'])) {
+ $completeMatch = $option['complete_match'];
+ }
+
+ $pattern = array_merge($this->parent->getPattern(), $this->pattern);
+ $depr = $this->router->config('pathinfo_depr');
+
+ // 检查完整规则定义
+ if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) {
+ return false;
+ }
+
+ $var = [];
+ $url = $depr . str_replace('|', $depr, $url);
+ $rule = $depr . str_replace('/', $depr, $this->rule);
+
+ if ($depr == $rule && $depr != $url) {
+ return false;
+ }
+
+ if (false === strpos($rule, '<')) {
+ if (0 === strcasecmp($rule, $url) || (!$completeMatch && 0 === strncasecmp($rule . $depr, $url . $depr, strlen($rule . $depr)))) {
+ return $var;
+ }
+ return false;
+ }
+
+ $slash = preg_quote('/-' . $depr, '/');
+
+ if ($matchRule = preg_split('/[' . $slash . ']?<\w+\??>/', $rule, 2)) {
+ if ($matchRule[0] && 0 !== strncasecmp($rule, $url, strlen($matchRule[0]))) {
+ return false;
+ }
+ }
+
+ if (preg_match_all('/[' . $slash . ']?\w+\??>?/', $rule, $matches)) {
+ $regex = $this->buildRuleRegex($rule, $matches[0], $pattern, $option, $completeMatch);
+
+ try {
+ if (!preg_match('/^' . $regex . ($completeMatch ? '$' : '') . '/u', $url, $match)) {
+ return false;
+ }
+ } catch (\Exception $e) {
+ throw new Exception('route pattern error');
+ }
+
+ foreach ($match as $key => $val) {
+ if (is_string($key)) {
+ $var[$key] = $val;
+ }
+ }
+ }
+
+ // 成功匹配后返回URL中的动态变量数组
+ return $var;
+ }
+
+}
diff --git a/thinkphp/library/think/route/RuleName.php b/thinkphp/library/think/route/RuleName.php
new file mode 100644
index 00000000..c73c3639
--- /dev/null
+++ b/thinkphp/library/think/route/RuleName.php
@@ -0,0 +1,136 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route;
+
+class RuleName
+{
+ protected $item = [];
+ protected $rule = [];
+
+ /**
+ * 注册路由标识
+ * @access public
+ * @param string $name 路由标识
+ * @param array $value 路由规则
+ * @param bool $first 是否置顶
+ * @return void
+ */
+ public function set($name, $value, $first = false)
+ {
+ if ($first && isset($this->item[$name])) {
+ array_unshift($this->item[$name], $value);
+ } else {
+ $this->item[$name][] = $value;
+ }
+ }
+
+ /**
+ * 注册路由规则
+ * @access public
+ * @param string $rule 路由规则
+ * @param RuleItem $route 路由
+ * @return void
+ */
+ public function setRule($rule, $route)
+ {
+ $this->rule[$route->getDomain()][$rule][$route->getRoute()] = $route;
+ }
+
+ /**
+ * 根据路由规则获取路由对象(列表)
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @return array
+ */
+ public function getRule($rule, $domain = null)
+ {
+ return isset($this->rule[$domain][$rule]) ? $this->rule[$domain][$rule] : [];
+ }
+
+ /**
+ * 获取全部路由列表
+ * @access public
+ * @param string $domain 域名
+ * @return array
+ */
+ public function getRuleList($domain = null)
+ {
+ $list = [];
+
+ foreach ($this->rule as $ruleDomain => $rules) {
+ foreach ($rules as $rule => $items) {
+ foreach ($items as $item) {
+ $val = [];
+
+ foreach (['method', 'rule', 'name', 'route', 'pattern', 'option'] as $param) {
+ $call = 'get' . $param;
+ $val[$param] = $item->$call();
+ }
+
+ $list[$ruleDomain][] = $val;
+ }
+ }
+ }
+
+ if ($domain) {
+ return isset($list[$domain]) ? $list[$domain] : [];
+ }
+
+ return $list;
+ }
+
+ /**
+ * 导入路由标识
+ * @access public
+ * @param array $name 路由标识
+ * @return void
+ */
+ public function import($item)
+ {
+ $this->item = $item;
+ }
+
+ /**
+ * 根据路由标识获取路由信息(用于URL生成)
+ * @access public
+ * @param string $name 路由标识
+ * @param string $domain 域名
+ * @return array|null
+ */
+ public function get($name = null, $domain = null)
+ {
+ if (is_null($name)) {
+ return $this->item;
+ }
+
+ $name = strtolower($name);
+
+ if (isset($this->item[$name])) {
+ if (is_null($domain)) {
+ $result = $this->item[$name];
+ } else {
+ $result = [];
+ foreach ($this->item[$name] as $item) {
+ if ($item[2] == $domain) {
+ $result[] = $item;
+ }
+ }
+ }
+ } else {
+ $result = null;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/thinkphp/library/think/route/dispatch/Callback.php b/thinkphp/library/think/route/dispatch/Callback.php
new file mode 100644
index 00000000..ca76fc99
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Callback.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\route\Dispatch;
+
+class Callback extends Dispatch
+{
+ public function exec()
+ {
+ // 执行回调方法
+ $vars = array_merge($this->request->param(), $this->param);
+
+ return $this->app->invoke($this->dispatch, $vars);
+ }
+
+}
diff --git a/thinkphp/library/think/route/dispatch/Controller.php b/thinkphp/library/think/route/dispatch/Controller.php
new file mode 100644
index 00000000..1de82992
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Controller.php
@@ -0,0 +1,30 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\route\Dispatch;
+
+class Controller extends Dispatch
+{
+ public function exec()
+ {
+ // 执行控制器的操作方法
+ $vars = array_merge($this->request->param(), $this->param);
+
+ return $this->app->action(
+ $this->dispatch, $vars,
+ $this->rule->getConfig('url_controller_layer'),
+ $this->rule->getConfig('controller_suffix')
+ );
+ }
+
+}
diff --git a/thinkphp/library/think/route/dispatch/Module.php b/thinkphp/library/think/route/dispatch/Module.php
new file mode 100644
index 00000000..4224b362
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Module.php
@@ -0,0 +1,136 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use ReflectionMethod;
+use think\exception\ClassNotFoundException;
+use think\exception\HttpException;
+use think\Loader;
+use think\Request;
+use think\route\Dispatch;
+
+class Module extends Dispatch
+{
+ protected $controller;
+ protected $actionName;
+
+ public function init()
+ {
+ parent::init();
+
+ $result = $this->dispatch;
+
+ if (is_string($result)) {
+ $result = explode('/', $result);
+ }
+
+ if ($this->rule->getConfig('app_multi_module')) {
+ // 多模块部署
+ $module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
+ $bind = $this->rule->getRouter()->getBind();
+ $available = false;
+
+ if ($bind && preg_match('/^[a-z]/is', $bind)) {
+ // 绑定模块
+ list($bindModule) = explode('/', $bind);
+ if (empty($result[0])) {
+ $module = $bindModule;
+ }
+ $available = true;
+ } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
+ $available = true;
+ } elseif ($this->rule->getConfig('empty_module')) {
+ $module = $this->rule->getConfig('empty_module');
+ $available = true;
+ }
+
+ // 模块初始化
+ if ($module && $available) {
+ // 初始化模块
+ $this->request->setModule($module);
+ $this->app->init($module);
+ } else {
+ throw new HttpException(404, 'module not exists:' . $module);
+ }
+ }
+
+ // 是否自动转换控制器和操作名
+ $convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert');
+ // 获取控制器名
+ $controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
+ $this->controller = $convert ? strtolower($controller) : $controller;
+
+ // 获取操作名
+ $this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action'));
+
+ // 设置当前请求的控制器、操作
+ $this->request
+ ->setController(Loader::parseName($this->controller, 1))
+ ->setAction($this->actionName);
+
+ return $this;
+ }
+
+ public function exec()
+ {
+ // 监听module_init
+ $this->app['hook']->listen('module_init');
+
+ try {
+ // 实例化控制器
+ $instance = $this->app->controller($this->controller,
+ $this->rule->getConfig('url_controller_layer'),
+ $this->rule->getConfig('controller_suffix'),
+ $this->rule->getConfig('empty_controller'));
+ } catch (ClassNotFoundException $e) {
+ throw new HttpException(404, 'controller not exists:' . $e->getClass());
+ }
+
+ $this->app['middleware']->controller(function (Request $request, $next) use ($instance) {
+ // 获取当前操作名
+ $action = $this->actionName . $this->rule->getConfig('action_suffix');
+
+ if (is_callable([$instance, $action])) {
+ // 执行操作方法
+ $call = [$instance, $action];
+
+ // 严格获取当前操作方法名
+ $reflect = new ReflectionMethod($instance, $action);
+ $methodName = $reflect->getName();
+ $suffix = $this->rule->getConfig('action_suffix');
+ $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
+ $this->request->setAction($actionName);
+
+ // 自动获取请求变量
+ $vars = $this->rule->getConfig('url_param_type')
+ ? $this->request->route()
+ : $this->request->param();
+ } elseif (is_callable([$instance, '_empty'])) {
+ // 空操作
+ $call = [$instance, '_empty'];
+ $vars = [$this->actionName];
+ $reflect = new ReflectionMethod($instance, '_empty');
+ } else {
+ // 操作不存在
+ throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
+ }
+
+ $this->app['hook']->listen('action_begin', $call);
+
+ $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
+
+ return $this->autoResponse($data);
+ });
+
+ return $this->app['middleware']->dispatch($this->request, 'controller');
+ }
+}
diff --git a/thinkphp/library/think/route/dispatch/Redirect.php b/thinkphp/library/think/route/dispatch/Redirect.php
new file mode 100644
index 00000000..fae2c9a6
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Redirect.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\Response;
+use think\route\Dispatch;
+
+class Redirect extends Dispatch
+{
+ public function exec()
+ {
+ return Response::create($this->dispatch, 'redirect')->code($this->code);
+ }
+}
diff --git a/thinkphp/library/think/route/dispatch/Response.php b/thinkphp/library/think/route/dispatch/Response.php
new file mode 100644
index 00000000..66f4e5ab
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Response.php
@@ -0,0 +1,23 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\route\Dispatch;
+
+class Response extends Dispatch
+{
+ public function exec()
+ {
+ return $this->dispatch;
+ }
+
+}
diff --git a/thinkphp/library/think/route/dispatch/Url.php b/thinkphp/library/think/route/dispatch/Url.php
new file mode 100644
index 00000000..0fb6e0c2
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/Url.php
@@ -0,0 +1,163 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\exception\HttpException;
+use think\Loader;
+use think\route\Dispatch;
+
+class Url extends Dispatch
+{
+ public function init()
+ {
+ // 解析默认的URL规则
+ $result = $this->parseUrl($this->dispatch);
+
+ return (new Module($this->request, $this->rule, $result))->init();
+ }
+
+ public function exec()
+ {}
+
+ /**
+ * 解析URL地址
+ * @access protected
+ * @param string $url URL
+ * @return array
+ */
+ protected function parseUrl($url)
+ {
+ $depr = $this->rule->getConfig('pathinfo_depr');
+ $bind = $this->rule->getRouter()->getBind();
+
+ if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) {
+ $bind = str_replace('/', $depr, $bind);
+ // 如果有模块/控制器绑定
+ $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
+ }
+
+ list($path, $var) = $this->rule->parseUrlPath($url);
+ if (empty($path)) {
+ return [null, null, null];
+ }
+
+ // 解析模块
+ $module = $this->rule->getConfig('app_multi_module') ? array_shift($path) : null;
+
+ if ($this->param['auto_search']) {
+ $controller = $this->autoFindController($module, $path);
+ } else {
+ // 解析控制器
+ $controller = !empty($path) ? array_shift($path) : null;
+ }
+
+ // 解析操作
+ $action = !empty($path) ? array_shift($path) : null;
+
+ // 解析额外参数
+ if ($path) {
+ if ($this->rule->getConfig('url_param_type')) {
+ $var += $path;
+ } else {
+ preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
+ $var[$match[1]] = strip_tags($match[2]);
+ }, implode('|', $path));
+ }
+ }
+
+ $panDomain = $this->request->panDomain();
+
+ if ($panDomain && $key = array_search('*', $var)) {
+ // 泛域名赋值
+ $var[$key] = $panDomain;
+ }
+
+ // 设置当前请求的参数
+ $this->request->setRouteVars($var);
+
+ // 封装路由
+ $route = [$module, $controller, $action];
+
+ if ($this->hasDefinedRoute($route, $bind)) {
+ throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
+ }
+
+ return $route;
+ }
+
+ /**
+ * 检查URL是否已经定义过路由
+ * @access protected
+ * @param string $route 路由信息
+ * @param string $bind 绑定信息
+ * @return bool
+ */
+ protected function hasDefinedRoute($route, $bind)
+ {
+ list($module, $controller, $action) = $route;
+
+ // 检查地址是否被定义过路由
+ $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
+
+ $name2 = '';
+
+ if (empty($module) || $module == $bind) {
+ $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
+ }
+
+ $host = $this->request->host(true);
+
+ if ($this->rule->getRouter()->getName($name, $host) || $this->rule->getRouter()->getName($name2, $host)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 自动定位控制器类
+ * @access protected
+ * @param string $module 模块名
+ * @param array $path URL
+ * @return string
+ */
+ protected function autoFindController($module, &$path)
+ {
+ $dir = $this->app->getAppPath() . ($module ? $module . '/' : '') . $this->rule->getConfig('url_controller_layer');
+ $suffix = $this->app->getSuffix() || $this->rule->getConfig('controller_suffix') ? ucfirst($this->rule->getConfig('url_controller_layer')) : '';
+
+ $item = [];
+ $find = false;
+
+ foreach ($path as $val) {
+ $item[] = $val;
+ $file = $dir . '/' . str_replace('.', '/', $val) . $suffix . '.php';
+ $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . '.php';
+ if (is_file($file)) {
+ $find = true;
+ break;
+ } else {
+ $dir .= '/' . Loader::parseName($val);
+ }
+ }
+
+ if ($find) {
+ $controller = implode('.', $item);
+ $path = array_slice($path, count($item));
+ } else {
+ $controller = array_shift($path);
+ }
+
+ return $controller;
+ }
+
+}
diff --git a/thinkphp/library/think/route/dispatch/View.php b/thinkphp/library/think/route/dispatch/View.php
new file mode 100644
index 00000000..ea3ef11b
--- /dev/null
+++ b/thinkphp/library/think/route/dispatch/View.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\route\dispatch;
+
+use think\Response;
+use think\route\Dispatch;
+
+class View extends Dispatch
+{
+ public function exec()
+ {
+ // 渲染模板输出
+ $vars = array_merge($this->request->param(), $this->param);
+
+ return Response::create($this->dispatch, 'view')->assign($vars);
+ }
+}
diff --git a/thinkphp/library/think/session/driver/Memcache.php b/thinkphp/library/think/session/driver/Memcache.php
new file mode 100644
index 00000000..40d7bb82
--- /dev/null
+++ b/thinkphp/library/think/session/driver/Memcache.php
@@ -0,0 +1,124 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\session\driver;
+
+use SessionHandlerInterface;
+use think\Exception;
+
+class Memcache implements SessionHandlerInterface
+{
+ protected $handler = null;
+ protected $config = [
+ 'host' => '127.0.0.1', // memcache主机
+ 'port' => 11211, // memcache端口
+ 'expire' => 3600, // session有效期
+ 'timeout' => 0, // 连接超时时间(单位:毫秒)
+ 'persistent' => true, // 长连接
+ 'session_name' => '', // memcache key前缀
+ ];
+
+ public function __construct($config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 打开Session
+ * @access public
+ * @param string $savePath
+ * @param mixed $sessName
+ */
+ public function open($savePath, $sessName)
+ {
+ // 检测php环境
+ if (!extension_loaded('memcache')) {
+ throw new Exception('not support:memcache');
+ }
+
+ $this->handler = new \Memcache;
+
+ // 支持集群
+ $hosts = explode(',', $this->config['host']);
+ $ports = explode(',', $this->config['port']);
+
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ foreach ((array) $hosts as $i => $host) {
+ $port = isset($ports[$i]) ? $ports[$i] : $ports[0];
+ $this->config['timeout'] > 0 ?
+ $this->handler->addServer($host, $port, $this->config['persistent'], 1, $this->config['timeout']) :
+ $this->handler->addServer($host, $port, $this->config['persistent'], 1);
+ }
+
+ return true;
+ }
+
+ /**
+ * 关闭Session
+ * @access public
+ */
+ public function close()
+ {
+ $this->gc(ini_get('session.gc_maxlifetime'));
+ $this->handler->close();
+ $this->handler = null;
+
+ return true;
+ }
+
+ /**
+ * 读取Session
+ * @access public
+ * @param string $sessID
+ */
+ public function read($sessID)
+ {
+ return (string) $this->handler->get($this->config['session_name'] . $sessID);
+ }
+
+ /**
+ * 写入Session
+ * @access public
+ * @param string $sessID
+ * @param string $sessData
+ * @return bool
+ */
+ public function write($sessID, $sessData)
+ {
+ return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
+ }
+
+ /**
+ * 删除Session
+ * @access public
+ * @param string $sessID
+ * @return bool
+ */
+ public function destroy($sessID)
+ {
+ return $this->handler->delete($this->config['session_name'] . $sessID);
+ }
+
+ /**
+ * Session 垃圾回收
+ * @access public
+ * @param string $sessMaxLifeTime
+ * @return true
+ */
+ public function gc($sessMaxLifeTime)
+ {
+ return true;
+ }
+}
diff --git a/thinkphp/library/think/session/driver/Memcached.php b/thinkphp/library/think/session/driver/Memcached.php
new file mode 100644
index 00000000..074b2ff7
--- /dev/null
+++ b/thinkphp/library/think/session/driver/Memcached.php
@@ -0,0 +1,135 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\session\driver;
+
+use SessionHandlerInterface;
+use think\Exception;
+
+class Memcached implements SessionHandlerInterface
+{
+ protected $handler = null;
+ protected $config = [
+ 'host' => '127.0.0.1', // memcache主机
+ 'port' => 11211, // memcache端口
+ 'expire' => 3600, // session有效期
+ 'timeout' => 0, // 连接超时时间(单位:毫秒)
+ 'session_name' => '', // memcache key前缀
+ 'username' => '', //账号
+ 'password' => '', //密码
+ ];
+
+ public function __construct($config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 打开Session
+ * @access public
+ * @param string $savePath
+ * @param mixed $sessName
+ */
+ public function open($savePath, $sessName)
+ {
+ // 检测php环境
+ if (!extension_loaded('memcached')) {
+ throw new Exception('not support:memcached');
+ }
+
+ $this->handler = new \Memcached;
+
+ // 设置连接超时时间(单位:毫秒)
+ if ($this->config['timeout'] > 0) {
+ $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->config['timeout']);
+ }
+
+ // 支持集群
+ $hosts = explode(',', $this->config['host']);
+ $ports = explode(',', $this->config['port']);
+
+ if (empty($ports[0])) {
+ $ports[0] = 11211;
+ }
+
+ // 建立连接
+ $servers = [];
+ foreach ((array) $hosts as $i => $host) {
+ $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1];
+ }
+
+ $this->handler->addServers($servers);
+
+ if ('' != $this->config['username']) {
+ $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $this->handler->setSaslAuthData($this->config['username'], $this->config['password']);
+ }
+
+ return true;
+ }
+
+ /**
+ * 关闭Session
+ * @access public
+ */
+ public function close()
+ {
+ $this->gc(ini_get('session.gc_maxlifetime'));
+ $this->handler->quit();
+ $this->handler = null;
+
+ return true;
+ }
+
+ /**
+ * 读取Session
+ * @access public
+ * @param string $sessID
+ */
+ public function read($sessID)
+ {
+ return (string) $this->handler->get($this->config['session_name'] . $sessID);
+ }
+
+ /**
+ * 写入Session
+ * @access public
+ * @param string $sessID
+ * @param string $sessData
+ * @return bool
+ */
+ public function write($sessID, $sessData)
+ {
+ return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
+ }
+
+ /**
+ * 删除Session
+ * @access public
+ * @param string $sessID
+ * @return bool
+ */
+ public function destroy($sessID)
+ {
+ return $this->handler->delete($this->config['session_name'] . $sessID);
+ }
+
+ /**
+ * Session 垃圾回收
+ * @access public
+ * @param string $sessMaxLifeTime
+ * @return true
+ */
+ public function gc($sessMaxLifeTime)
+ {
+ return true;
+ }
+}
diff --git a/thinkphp/library/think/session/driver/Redis.php b/thinkphp/library/think/session/driver/Redis.php
new file mode 100644
index 00000000..a521ca0f
--- /dev/null
+++ b/thinkphp/library/think/session/driver/Redis.php
@@ -0,0 +1,177 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\session\driver;
+
+use SessionHandlerInterface;
+use think\Exception;
+
+class Redis implements SessionHandlerInterface
+{
+ /** @var \Redis */
+ protected $handler = null;
+ protected $config = [
+ 'host' => '127.0.0.1', // redis主机
+ 'port' => 6379, // redis端口
+ 'password' => '', // 密码
+ 'select' => 0, // 操作库
+ 'expire' => 3600, // 有效期(秒)
+ 'timeout' => 0, // 超时时间(秒)
+ 'persistent' => true, // 是否长连接
+ 'session_name' => '', // sessionkey前缀
+ ];
+
+ public function __construct($config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+ }
+
+ /**
+ * 打开Session
+ * @access public
+ * @param string $savePath
+ * @param mixed $sessName
+ * @return bool
+ * @throws Exception
+ */
+ public function open($savePath, $sessName)
+ {
+ if (extension_loaded('redis')) {
+ $this->handler = new \Redis;
+
+ // 建立连接
+ $func = $this->config['persistent'] ? 'pconnect' : 'connect';
+ $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']);
+
+ if ('' != $this->config['password']) {
+ $this->handler->auth($this->config['password']);
+ }
+
+ if (0 != $this->config['select']) {
+ $this->handler->select($this->config['select']);
+ }
+ } elseif (class_exists('\Predis\Client')) {
+ $params = [];
+ foreach ($this->config as $key => $val) {
+ if (in_array($key, ['aggregate', 'cluster', 'connections', 'exceptions', 'prefix', 'profile', 'replication'])) {
+ $params[$key] = $val;
+ unset($this->config[$key]);
+ }
+ }
+ $this->handler = new \Predis\Client($this->config, $params);
+ } else {
+ throw new \BadFunctionCallException('not support: redis');
+ }
+
+ return true;
+ }
+
+ /**
+ * 关闭Session
+ * @access public
+ */
+ public function close()
+ {
+ $this->gc(ini_get('session.gc_maxlifetime'));
+ $this->handler->close();
+ $this->handler = null;
+
+ return true;
+ }
+
+ /**
+ * 读取Session
+ * @access public
+ * @param string $sessID
+ * @return string
+ */
+ public function read($sessID)
+ {
+ return (string) $this->handler->get($this->config['session_name'] . $sessID);
+ }
+
+ /**
+ * 写入Session
+ * @access public
+ * @param string $sessID
+ * @param string $sessData
+ * @return bool
+ */
+ public function write($sessID, $sessData)
+ {
+ if ($this->config['expire'] > 0) {
+ return $this->handler->setex($this->config['session_name'] . $sessID, $this->config['expire'], $sessData);
+ } else {
+ return $this->handler->set($this->config['session_name'] . $sessID, $sessData);
+ }
+ }
+
+ /**
+ * 删除Session
+ * @access public
+ * @param string $sessID
+ * @return bool
+ */
+ public function destroy($sessID)
+ {
+ return $this->handler->delete($this->config['session_name'] . $sessID) > 0;
+ }
+
+ /**
+ * Session 垃圾回收
+ * @access public
+ * @param string $sessMaxLifeTime
+ * @return bool
+ */
+ public function gc($sessMaxLifeTime)
+ {
+ return true;
+ }
+
+ /**
+ * Redis Session 驱动的加锁机制
+ * @access public
+ * @param string $sessID 用于加锁的sessID
+ * @param integer $timeout 默认过期时间
+ * @return bool
+ */
+ public function lock($sessID, $timeout = 10)
+ {
+ if (null == $this->handler) {
+ $this->open('', '');
+ }
+
+ $lockKey = 'LOCK_PREFIX_' . $sessID;
+ // 使用setnx操作加锁
+ $isLock = $this->handler->setnx($lockKey, 1);
+ if ($isLock) {
+ // 设置过期时间,防止死任务的出现
+ $this->handler->expire($lockKey, $timeout);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Redis Session 驱动的解锁机制
+ * @access public
+ * @param string $sessID 用于解锁的sessID
+ */
+ public function unlock($sessID)
+ {
+ if (null == $this->handler) {
+ $this->open('', '');
+ }
+
+ $this->handler->del('LOCK_PREFIX_' . $sessID);
+ }
+}
diff --git a/thinkphp/library/think/template/TagLib.php b/thinkphp/library/think/template/TagLib.php
new file mode 100644
index 00000000..3653b7d2
--- /dev/null
+++ b/thinkphp/library/think/template/TagLib.php
@@ -0,0 +1,351 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template;
+
+use think\Exception;
+
+/**
+ * ThinkPHP标签库TagLib解析基类
+ * @category Think
+ * @package Think
+ * @subpackage Template
+ * @author liu21st
+ */
+class TagLib
+{
+
+ /**
+ * 标签库定义XML文件
+ * @var string
+ * @access protected
+ */
+ protected $xml = '';
+ protected $tags = []; // 标签定义
+ /**
+ * 标签库名称
+ * @var string
+ * @access protected
+ */
+ protected $tagLib = '';
+
+ /**
+ * 标签库标签列表
+ * @var array
+ * @access protected
+ */
+ protected $tagList = [];
+
+ /**
+ * 标签库分析数组
+ * @var array
+ * @access protected
+ */
+ protected $parse = [];
+
+ /**
+ * 标签库是否有效
+ * @var bool
+ * @access protected
+ */
+ protected $valid = false;
+
+ /**
+ * 当前模板对象
+ * @var object
+ * @access protected
+ */
+ protected $tpl;
+
+ protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < '];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param \stdClass $template 模板引擎对象
+ */
+ public function __construct($template)
+ {
+ $this->tpl = $template;
+ }
+
+ /**
+ * 按签标库替换页面中的标签
+ * @access public
+ * @param string $content 模板内容
+ * @param string $lib 标签库名
+ * @return void
+ */
+ public function parseTag(&$content, $lib = '')
+ {
+ $tags = [];
+ $lib = $lib ? strtolower($lib) . ':' : '';
+
+ foreach ($this->tags as $name => $val) {
+ $close = !isset($val['close']) || $val['close'] ? 1 : 0;
+ $tags[$close][$lib . $name] = $name;
+ if (isset($val['alias'])) {
+ // 别名设置
+ $array = (array) $val['alias'];
+ foreach (explode(',', $array[0]) as $v) {
+ $tags[$close][$lib . $v] = $name;
+ }
+ }
+ }
+
+ // 闭合标签
+ if (!empty($tags[1])) {
+ $nodes = [];
+ $regex = $this->getRegex(array_keys($tags[1]), 1);
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $right = [];
+ foreach ($matches as $match) {
+ if ('' == $match[1][0]) {
+ $name = strtolower($match[2][0]);
+ // 如果有没闭合的标签头则取出最后一个
+ if (!empty($right[$name])) {
+ // $match[0][1]为标签结束符在模板中的位置
+ $nodes[$match[0][1]] = [
+ 'name' => $name,
+ 'begin' => array_pop($right[$name]), // 标签开始符
+ 'end' => $match[0], // 标签结束符
+ ];
+ }
+ } else {
+ // 标签头压入栈
+ $right[strtolower($match[1][0])][] = $match[0];
+ }
+ }
+ unset($right, $matches);
+ // 按标签在模板中的位置从后向前排序
+ krsort($nodes);
+ }
+
+ $break = '';
+ if ($nodes) {
+ $beginArray = [];
+ // 标签替换 从后向前
+ foreach ($nodes as $pos => $node) {
+ // 对应的标签名
+ $name = $tags[1][$node['name']];
+ $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : '';
+
+ // 解析标签属性
+ $attrs = $this->parseAttr($node['begin'][0], $name, $alias);
+ $method = 'tag' . $name;
+
+ // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾
+ $replace = explode($break, $this->$method($attrs, $break));
+
+ if (count($replace) > 1) {
+ while ($beginArray) {
+ $begin = end($beginArray);
+ // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签
+ if ($node['end'][1] > $begin['pos']) {
+ break;
+ } else {
+ // 不为子标签时,取出栈中最后一个标签头
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ // 替换标签尾部
+ $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0]));
+ // 把标签头压入栈
+ $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]];
+ }
+ }
+
+ while ($beginArray) {
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ }
+ // 自闭合标签
+ if (!empty($tags[0])) {
+ $regex = $this->getRegex(array_keys($tags[0]), 0);
+ $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) {
+ // 对应的标签名
+ $name = $tags[0][strtolower($matches[1])];
+ $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : '';
+ // 解析标签属性
+ $attrs = $this->parseAttr($matches[0], $name, $alias);
+ $method = 'tag' . $name;
+ return $this->$method($attrs, '');
+ }, $content);
+ }
+
+ return;
+ }
+
+ /**
+ * 按标签生成正则
+ * @access public
+ * @param array|string $tags 标签名
+ * @param boolean $close 是否为闭合标签
+ * @return string
+ */
+ public function getRegex($tags, $close)
+ {
+ $begin = $this->tpl->config('taglib_begin');
+ $end = $this->tpl->config('taglib_end');
+ $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
+ $tagName = is_array($tags) ? implode('|', $tags) : $tags;
+
+ if ($single) {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end;
+ }
+ } else {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end;
+ }
+ }
+
+ return '/' . $regex . '/is';
+ }
+
+ /**
+ * 分析标签属性 正则方式
+ * @access public
+ * @param string $str 标签属性字符串
+ * @param string $name 标签名
+ * @param string $alias 别名
+ * @return array
+ */
+ public function parseAttr($str, $name, $alias = '')
+ {
+ $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is';
+ $result = [];
+
+ if (preg_match_all($regex, $str, $matches)) {
+ foreach ($matches['name'] as $key => $val) {
+ $result[$val] = $matches['value'][$key];
+ }
+
+ if (!isset($this->tags[$name])) {
+ // 检测是否存在别名定义
+ foreach ($this->tags as $key => $val) {
+ if (isset($val['alias'])) {
+ $array = (array) $val['alias'];
+ if (in_array($name, explode(',', $array[0]))) {
+ $tag = $val;
+ $type = !empty($array[1]) ? $array[1] : 'type';
+ $result[$type] = $name;
+ break;
+ }
+ }
+ }
+ } else {
+ $tag = $this->tags[$name];
+ // 设置了标签别名
+ if (!empty($alias) && isset($tag['alias'])) {
+ $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type';
+ $result[$type] = $alias;
+ }
+ }
+
+ if (!empty($tag['must'])) {
+ $must = explode(',', $tag['must']);
+ foreach ($must as $name) {
+ if (!isset($result[$name])) {
+ throw new Exception('tag attr must:' . $name);
+ }
+ }
+ }
+ } else {
+ // 允许直接使用表达式的标签
+ if (!empty($this->tags[$name]['expression'])) {
+ static $_taglibs;
+ if (!isset($_taglibs[$name])) {
+ $_taglibs[$name][0] = strlen($this->tpl->config('taglib_begin_origin') . $name);
+ $_taglibs[$name][1] = strlen($this->tpl->config('taglib_end_origin'));
+ }
+ $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]);
+ // 清除自闭合标签尾部/
+ $result['expression'] = rtrim($result['expression'], '/');
+ $result['expression'] = trim($result['expression']);
+ } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) {
+ throw new Exception('tag error:' . $name);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析条件表达式
+ * @access public
+ * @param string $condition 表达式标签内容
+ * @return string
+ */
+ public function parseCondition($condition)
+ {
+ if (strpos($condition, ':')) {
+ $condition = ' ' . substr(strstr($condition, ':'), 1);
+ }
+
+ $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition);
+ $this->tpl->parseVar($condition);
+
+ // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异
+ return $condition;
+ }
+
+ /**
+ * 自动识别构建变量
+ * @access public
+ * @param string $name 变量描述
+ * @return string
+ */
+ public function autoBuildVar(&$name)
+ {
+ $flag = substr($name, 0, 1);
+
+ if (':' == $flag) {
+ // 以:开头为函数调用,解析前去掉:
+ $name = substr($name, 1);
+ } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) {
+ // XXX: 这句的写法可能还需要改进
+ // 常量不需要解析
+ if (defined($name)) {
+ return $name;
+ }
+
+ // 不以$开头并且也不是常量,自动补上$前缀
+ $name = '$' . $name;
+ }
+
+ $this->tpl->parseVar($name);
+ $this->tpl->parseVarFunction($name, false);
+
+ return $name;
+ }
+
+ /**
+ * 获取标签列表
+ * @access public
+ * @return array
+ */
+ public function getTags()
+ {
+ return $this->tags;
+ }
+}
diff --git a/thinkphp/library/think/template/driver/File.php b/thinkphp/library/think/template/driver/File.php
new file mode 100644
index 00000000..3b96a0f3
--- /dev/null
+++ b/thinkphp/library/think/template/driver/File.php
@@ -0,0 +1,83 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\driver;
+
+use think\Exception;
+
+class File
+{
+ protected $cacheFile;
+
+ /**
+ * 写入编译缓存
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param string $content 缓存的内容
+ * @return void|array
+ */
+ public function write($cacheFile, $content)
+ {
+ // 检测模板目录
+ $dir = dirname($cacheFile);
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ // 生成模板缓存文件
+ if (false === file_put_contents($cacheFile, $content)) {
+ throw new Exception('cache write error:' . $cacheFile, 11602);
+ }
+ }
+
+ /**
+ * 读取编译编译
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param array $vars 变量数组
+ * @return void
+ */
+ public function read($cacheFile, $vars = [])
+ {
+ $this->cacheFile = $cacheFile;
+
+ if (!empty($vars) && is_array($vars)) {
+ // 模板阵列变量分解成为独立变量
+ extract($vars, EXTR_OVERWRITE);
+ }
+
+ //载入模版缓存文件
+ include $this->cacheFile;
+ }
+
+ /**
+ * 检查编译缓存是否有效
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param int $cacheTime 缓存时间
+ * @return boolean
+ */
+ public function check($cacheFile, $cacheTime)
+ {
+ // 缓存文件不存在, 直接返回false
+ if (!file_exists($cacheFile)) {
+ return false;
+ }
+
+ if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) {
+ // 缓存是否在有效期
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/thinkphp/library/think/template/taglib/Cx.php b/thinkphp/library/think/template/taglib/Cx.php
new file mode 100644
index 00000000..ad741f28
--- /dev/null
+++ b/thinkphp/library/think/template/taglib/Cx.php
@@ -0,0 +1,724 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\taglib;
+
+use think\template\TagLib;
+
+/**
+ * CX标签库解析类
+ * @category Think
+ * @package Think
+ * @subpackage Driver.Taglib
+ * @author liu21st
+ */
+class Cx extends Taglib
+{
+
+ // 标签定义
+ protected $tags = [
+ // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次
+ 'php' => ['attr' => ''],
+ 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'],
+ 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true],
+ 'if' => ['attr' => 'condition', 'expression' => true],
+ 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true],
+ 'else' => ['attr' => '', 'close' => 0],
+ 'switch' => ['attr' => 'name', 'expression' => true],
+ 'case' => ['attr' => 'value,break', 'expression' => true],
+ 'default' => ['attr' => '', 'close' => 0],
+ 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']],
+ 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']],
+ 'empty' => ['attr' => 'name'],
+ 'notempty' => ['attr' => 'name'],
+ 'present' => ['attr' => 'name'],
+ 'notpresent' => ['attr' => 'name'],
+ 'defined' => ['attr' => 'name'],
+ 'notdefined' => ['attr' => 'name'],
+ 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']],
+ 'assign' => ['attr' => 'name,value', 'close' => 0],
+ 'define' => ['attr' => 'name,value', 'close' => 0],
+ 'for' => ['attr' => 'start,end,name,comparison,step'],
+ 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true],
+ 'function' => ['attr' => 'name,vars,use,call'],
+ ];
+
+ /**
+ * php标签解析
+ * 格式:
+ * {php}echo $name{/php}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPhp($tag, $content)
+ {
+ $parseStr = '';
+ return $parseStr;
+ }
+
+ /**
+ * volist标签解析 循环输出数据集
+ * 格式:
+ * {volist name="userList" id="user" empty=""}
+ * {user.username}
+ * {user.email}
+ * {/volist}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string|void
+ */
+ public function tagVolist($tag, $content)
+ {
+ $name = $tag['name'];
+ $id = $tag['id'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $key = !empty($tag['key']) ? $tag['key'] : 'i';
+ $mod = isset($tag['mod']) ? $tag['mod'] : '2';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+ // 允许使用函数设定数据集 {$vo.name}
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= '$_result=' . $name . ';';
+ $name = '$_result';
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $parseStr .= ' $__LIST__ = ' . $name . ';';
+ }
+
+ $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+ $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): ';
+ $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );';
+ $parseStr .= '++$' . $key . ';?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ if (!empty($parseStr)) {
+ return $parseStr;
+ }
+
+ return;
+ }
+
+ /**
+ * foreach标签解析 循环输出数据集
+ * 格式:
+ * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""}
+ * {user.username}
+ * {/foreach}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string|void
+ */
+ public function tagForeach($tag, $content)
+ {
+ // 直接使用表达式
+ if (!empty($tag['expression'])) {
+ $expression = ltrim(rtrim($tag['expression'], ')'), '(');
+ $expression = $this->autoBuildVar($expression);
+ $parseStr = '';
+ $parseStr .= $content;
+ $parseStr .= '';
+ return $parseStr;
+ }
+
+ $name = $tag['name'];
+ $key = !empty($tag['key']) ? $tag['key'] : 'key';
+ $item = !empty($tag['id']) ? $tag['id'] : $tag['item'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= $var . '=' . $name . '; ';
+ $name = $var;
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): ';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ if (!isset($var)) {
+ $var = '$_' . uniqid();
+ }
+ $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $var = &$name;
+ }
+
+ $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ $parseStr .= '$' . $index . '=0; ';
+ }
+
+ $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ if (isset($tag['mod'])) {
+ $mod = (int) $tag['mod'];
+ $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); ';
+ }
+ $parseStr .= '++$' . $index . '; ';
+ }
+
+ $parseStr .= '?>';
+ // 循环体中的内容
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ if (!empty($parseStr)) {
+ return $parseStr;
+ }
+
+ return;
+ }
+
+ /**
+ * if标签解析
+ * 格式:
+ * {if condition=" $a eq 1"}
+ * {elseif condition="$a eq 2" /}
+ * {else /}
+ * {/if}
+ * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || &&
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagIf($tag, $content)
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * elseif标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagElseif($tag, $content)
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * else标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @return string
+ */
+ public function tagElse($tag)
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * switch标签解析
+ * 格式:
+ * {switch name="a.name"}
+ * {case value="1" break="false"}1{/case}
+ * {case value="2" }2{/case}
+ * {default /}other
+ * {/switch}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagSwitch($tag, $content)
+ {
+ $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * case标签解析 需要配合switch才有效
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCase($tag, $content)
+ {
+ $value = isset($tag['expression']) ? $tag['expression'] : $tag['value'];
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $value = 'case ' . $value . ':';
+ } elseif (strpos($value, '|')) {
+ $values = explode('|', $value);
+ $value = '';
+ foreach ($values as $val) {
+ $value .= 'case "' . addslashes($val) . '":';
+ }
+ } else {
+ $value = 'case "' . $value . '":';
+ }
+
+ $parseStr = '' . $content;
+ $isBreak = isset($tag['break']) ? $tag['break'] : '';
+
+ if ('' == $isBreak || $isBreak) {
+ $parseStr .= '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * default标签解析 需要配合switch才有效
+ * 使用: {default /}ddfdf
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagDefault($tag)
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * compare标签解析
+ * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq
+ * 格式: {compare name="" type="eq" value="" }content{/compare}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCompare($tag, $content)
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ } else {
+ $value = '\'' . $value . '\'';
+ }
+
+ switch ($type) {
+ case 'equal':
+ $type = 'eq';
+ break;
+ case 'notequal':
+ $type = 'neq';
+ break;
+ }
+ $type = $this->parseCondition(' ' . $type . ' ');
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * range标签解析
+ * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外
+ * 格式: {range name="var|function" value="val" type='in|notin' }content{/range}
+ * example: {range name="a" value="1,2,3" type='in' }content{/range}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagRange($tag, $content)
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型
+
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')';
+ } else {
+ $value = '"' . $value . '"';
+ $str = 'explode(\',\',' . $value . ')';
+ }
+
+ if ('between' == $type) {
+ $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . '';
+ } elseif ('notbetween' == $type) {
+ $parseStr = '$_RANGE_VAR_[1]):?>' . $content . '';
+ } else {
+ $fun = ('in' == $type) ? 'in_array' : '!in_array';
+ $parseStr = '' . $content . '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * present标签解析
+ * 如果某个变量已经设置 则输出内容
+ * 格式: {present name="" }content{/present}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPresent($tag, $content)
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notpresent标签解析
+ * 如果某个变量没有设置,则输出内容
+ * 格式: {notpresent name="" }content{/notpresent}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotpresent($tag, $content)
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * empty标签解析
+ * 如果某个变量为empty 则输出内容
+ * 格式: {empty name="" }content{/empty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagEmpty($tag, $content)
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty())): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notempty标签解析
+ * 如果某个变量不为empty 则输出内容
+ * 格式: {notempty name="" }content{/notempty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotempty($tag, $content)
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty()))): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否已经定义了该常量
+ * {defined name='TXT'}已定义{/defined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagDefined($tag, $content)
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否没有定义了该常量
+ * {notdefined name='TXT'}已定义{/notdefined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagNotdefined($tag, $content)
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * load 标签解析 {load file="/static/js/base.js" /}
+ * 格式:{load file="/static/css/base.css" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagLoad($tag, $content)
+ {
+ $file = isset($tag['file']) ? $tag['file'] : $tag['href'];
+ $type = isset($tag['type']) ? strtolower($tag['type']) : '';
+
+ $parseStr = '';
+ $endStr = '';
+
+ // 判断是否存在加载条件 允许使用函数判断(默认为isset)
+ if (isset($tag['value'])) {
+ $name = $tag['value'];
+ $name = $this->autoBuildVar($name);
+ $name = 'isset(' . $name . ')';
+ $parseStr .= '';
+ $endStr = '';
+ }
+
+ // 文件方式导入
+ $array = explode(',', $file);
+
+ foreach ($array as $val) {
+ $type = strtolower(substr(strrchr($val, '.'), 1));
+ switch ($type) {
+ case 'js':
+ $parseStr .= '';
+ break;
+ case 'css':
+ $parseStr .= ' ';
+ break;
+ case 'php':
+ $parseStr .= '';
+ break;
+ }
+ }
+
+ return $parseStr . $endStr;
+ }
+
+ /**
+ * assign标签解析
+ * 在模板中给某个变量赋值 支持变量赋值
+ * 格式: {assign name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagAssign($tag, $content)
+ {
+ $name = $this->autoBuildVar($tag['name']);
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * define标签解析
+ * 在模板中定义常量 支持变量赋值
+ * 格式: {define name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagDefine($tag, $content)
+ {
+ $name = '\'' . $tag['name'] . '\'';
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * for标签解析
+ * 格式:
+ * {for start="" end="" comparison="" step="" name=""}
+ * content
+ * {/for}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFor($tag, $content)
+ {
+ //设置默认值
+ $start = 0;
+ $end = 0;
+ $step = 1;
+ $comparison = 'lt';
+ $name = 'i';
+ $rand = rand(); //添加随机数,防止嵌套变量冲突
+
+ //获取属性
+ foreach ($tag as $key => $value) {
+ $value = trim($value);
+ $flag = substr($value, 0, 1);
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ }
+
+ switch ($key) {
+ case 'start':
+ $start = $value;
+ break;
+ case 'end':
+ $end = $value;
+ break;
+ case 'step':
+ $step = $value;
+ break;
+ case 'comparison':
+ $comparison = $value;
+ break;
+ case 'name':
+ $name = $value;
+ break;
+ }
+ }
+
+ $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * url函数的tag标签
+ * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagUrl($tag, $content)
+ {
+ $url = isset($tag['link']) ? $tag['link'] : '';
+ $vars = isset($tag['vars']) ? $tag['vars'] : '';
+ $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true';
+ $domain = isset($tag['domain']) ? $tag['domain'] : 'false';
+
+ return '';
+ }
+
+ /**
+ * function标签解析 匿名函数,可实现递归
+ * 使用:
+ * {function name="func" vars="$data" call="$list" use="&$a,&$b"}
+ * {if is_array($data)}
+ * {foreach $data as $val}
+ * {~func($val) /}
+ * {/foreach}
+ * {else /}
+ * {$data}
+ * {/if}
+ * {/function}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFunction($tag, $content)
+ {
+ $name = !empty($tag['name']) ? $tag['name'] : 'func';
+ $vars = !empty($tag['vars']) ? $tag['vars'] : '';
+ $call = !empty($tag['call']) ? $tag['call'] : '';
+ $use = ['&$' . $name];
+
+ if (!empty($tag['use'])) {
+ foreach (explode(',', $tag['use']) as $val) {
+ $use[] = '&' . ltrim(trim($val), '&');
+ }
+ }
+
+ $parseStr = '' . $content . '' : '?>';
+
+ return $parseStr;
+ }
+}
diff --git a/thinkphp/library/think/validate/ValidateRule.php b/thinkphp/library/think/validate/ValidateRule.php
new file mode 100644
index 00000000..5253465f
--- /dev/null
+++ b/thinkphp/library/think/validate/ValidateRule.php
@@ -0,0 +1,171 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\validate;
+
+/**
+ * Class ValidateRule
+ * @package think\validate
+ * @method ValidateRule confirm(mixed $rule, string $msg = '') static 验证是否和某个字段的值一致
+ * @method ValidateRule different(mixed $rule, string $msg = '') static 验证是否和某个字段的值是否不同
+ * @method ValidateRule egt(mixed $rule, string $msg = '') static 验证是否大于等于某个值
+ * @method ValidateRule gt(mixed $rule, string $msg = '') static 验证是否大于某个值
+ * @method ValidateRule elt(mixed $rule, string $msg = '') static 验证是否小于等于某个值
+ * @method ValidateRule lt(mixed $rule, string $msg = '') static 验证是否小于某个值
+ * @method ValidateRule eg(mixed $rule, string $msg = '') static 验证是否等于某个值
+ * @method ValidateRule in(mixed $rule, string $msg = '') static 验证是否在范围内
+ * @method ValidateRule notIn(mixed $rule, string $msg = '') static 验证是否不在某个范围
+ * @method ValidateRule between(mixed $rule, string $msg = '') static 验证是否在某个区间
+ * @method ValidateRule notBetween(mixed $rule, string $msg = '') static 验证是否不在某个区间
+ * @method ValidateRule length(mixed $rule, string $msg = '') static 验证数据长度
+ * @method ValidateRule max(mixed $rule, string $msg = '') static 验证数据最大长度
+ * @method ValidateRule min(mixed $rule, string $msg = '') static 验证数据最小长度
+ * @method ValidateRule after(mixed $rule, string $msg = '') static 验证日期
+ * @method ValidateRule before(mixed $rule, string $msg = '') static 验证日期
+ * @method ValidateRule expire(mixed $rule, string $msg = '') static 验证有效期
+ * @method ValidateRule allowIp(mixed $rule, string $msg = '') static 验证IP许可
+ * @method ValidateRule denyIp(mixed $rule, string $msg = '') static 验证IP禁用
+ * @method ValidateRule regex(mixed $rule, string $msg = '') static 使用正则验证数据
+ * @method ValidateRule token(mixed $rule='__token__', string $msg = '') static 验证表单令牌
+ * @method ValidateRule is(mixed $rule, string $msg = '') static 验证字段值是否为有效格式
+ * @method ValidateRule isRequire(mixed $rule, string $msg = '') static 验证字段必须
+ * @method ValidateRule isNumber(mixed $rule, string $msg = '') static 验证字段值是否为数字
+ * @method ValidateRule isArray(mixed $rule, string $msg = '') static 验证字段值是否为数组
+ * @method ValidateRule isInteger(mixed $rule, string $msg = '') static 验证字段值是否为整形
+ * @method ValidateRule isFloat(mixed $rule, string $msg = '') static 验证字段值是否为浮点数
+ * @method ValidateRule isMobile(mixed $rule, string $msg = '') static 验证字段值是否为手机
+ * @method ValidateRule isIdCard(mixed $rule, string $msg = '') static 验证字段值是否为身份证号码
+ * @method ValidateRule isChs(mixed $rule, string $msg = '') static 验证字段值是否为中文
+ * @method ValidateRule isChsDash(mixed $rule, string $msg = '') static 验证字段值是否为中文字母及下划线
+ * @method ValidateRule isChsAlpha(mixed $rule, string $msg = '') static 验证字段值是否为中文和字母
+ * @method ValidateRule isChsAlphaNum(mixed $rule, string $msg = '') static 验证字段值是否为中文字母和数字
+ * @method ValidateRule isDate(mixed $rule, string $msg = '') static 验证字段值是否为有效格式
+ * @method ValidateRule isBool(mixed $rule, string $msg = '') static 验证字段值是否为布尔值
+ * @method ValidateRule isAlpha(mixed $rule, string $msg = '') static 验证字段值是否为字母
+ * @method ValidateRule isAlphaDash(mixed $rule, string $msg = '') static 验证字段值是否为字母和下划线
+ * @method ValidateRule isAlphaNum(mixed $rule, string $msg = '') static 验证字段值是否为字母和数字
+ * @method ValidateRule isAccepted(mixed $rule, string $msg = '') static 验证字段值是否为yes, on, 或是 1
+ * @method ValidateRule isEmail(mixed $rule, string $msg = '') static 验证字段值是否为有效邮箱格式
+ * @method ValidateRule isUrl(mixed $rule, string $msg = '') static 验证字段值是否为有效URL地址
+ * @method ValidateRule activeUrl(mixed $rule, string $msg = '') static 验证是否为合格的域名或者IP
+ * @method ValidateRule ip(mixed $rule, string $msg = '') static 验证是否有效IP
+ * @method ValidateRule fileExt(mixed $rule, string $msg = '') static 验证文件后缀
+ * @method ValidateRule fileMime(mixed $rule, string $msg = '') static 验证文件类型
+ * @method ValidateRule fileSize(mixed $rule, string $msg = '') static 验证文件大小
+ * @method ValidateRule image(mixed $rule, string $msg = '') static 验证图像文件
+ * @method ValidateRule method(mixed $rule, string $msg = '') static 验证请求类型
+ * @method ValidateRule dateFormat(mixed $rule, string $msg = '') static 验证时间和日期是否符合指定格式
+ * @method ValidateRule unique(mixed $rule, string $msg = '') static 验证是否唯一
+ * @method ValidateRule behavior(mixed $rule, string $msg = '') static 使用行为类验证
+ * @method ValidateRule filter(mixed $rule, string $msg = '') static 使用filter_var方式验证
+ * @method ValidateRule requireIf(mixed $rule, string $msg = '') static 验证某个字段等于某个值的时候必须
+ * @method ValidateRule requireCallback(mixed $rule, string $msg = '') static 通过回调方法验证某个字段是否必须
+ * @method ValidateRule requireWith(mixed $rule, string $msg = '') static 验证某个字段有值的情况下必须
+ * @method ValidateRule must(mixed $rule=null, string $msg = '') static 必须验证
+ */
+class ValidateRule
+{
+ // 验证字段的名称
+ protected $title;
+
+ // 当前验证规则
+ protected $rule = [];
+
+ // 验证提示信息
+ protected $message = [];
+
+ /**
+ * 添加验证因子
+ * @access protected
+ * @param string $name 验证名称
+ * @param mixed $rule 验证规则
+ * @param string $msg 提示信息
+ * @return $this
+ */
+ protected function addItem($name, $rule = null, $msg = '')
+ {
+ if ($rule || 0 === $rule) {
+ $this->rule[$name] = $rule;
+ } else {
+ $this->rule[] = $name;
+ }
+
+ $this->message[] = $msg;
+
+ return $this;
+ }
+
+ /**
+ * 获取验证规则
+ * @access public
+ * @return array
+ */
+ public function getRule()
+ {
+ return $this->rule;
+ }
+
+ /**
+ * 获取验证字段名称
+ * @access public
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * 获取验证提示
+ * @access public
+ * @return array
+ */
+ public function getMsg()
+ {
+ return $this->message;
+ }
+
+ /**
+ * 设置验证字段名称
+ * @access public
+ * @return $this
+ */
+ public function title($title)
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ public function __call($method, $args)
+ {
+ if ('is' == strtolower(substr($method, 0, 2))) {
+ $method = substr($method, 2);
+ }
+
+ array_unshift($args, lcfirst($method));
+
+ return call_user_func_array([$this, 'addItem'], $args);
+ }
+
+ public static function __callStatic($method, $args)
+ {
+ $rule = new static();
+
+ if ('is' == strtolower(substr($method, 0, 2))) {
+ $method = substr($method, 2);
+ }
+
+ array_unshift($args, lcfirst($method));
+
+ return call_user_func_array([$rule, 'addItem'], $args);
+ }
+}
diff --git a/thinkphp/library/think/view/driver/Php.php b/thinkphp/library/think/view/driver/Php.php
new file mode 100644
index 00000000..b6f6a3d0
--- /dev/null
+++ b/thinkphp/library/think/view/driver/Php.php
@@ -0,0 +1,179 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\view\driver;
+
+use think\App;
+use think\exception\TemplateNotFoundException;
+use think\Loader;
+
+class Php
+{
+ // 模板引擎参数
+ protected $config = [
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写
+ 'auto_rule' => 1,
+ // 视图基础目录(集中式)
+ 'view_base' => '',
+ // 模板起始路径
+ 'view_path' => '',
+ // 模板文件后缀
+ 'view_suffix' => 'php',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ ];
+
+ protected $template;
+ protected $app;
+ protected $content;
+
+ public function __construct(App $app, $config = [])
+ {
+ $this->app = $app;
+ $this->config = array_merge($this->config, (array) $config);
+ }
+
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists($template)
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ return is_file($template);
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function fetch($template, $data = [])
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ // 模板不存在 抛出异常
+ if (!is_file($template)) {
+ throw new TemplateNotFoundException('template not exists:' . $template, $template);
+ }
+
+ $this->template = $template;
+
+ // 记录视图信息
+ $this->app
+ ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]');
+
+ extract($data, EXTR_OVERWRITE);
+ include $this->template;
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $content 模板内容
+ * @param array $data 模板变量
+ * @return void
+ */
+ public function display($content, $data = [])
+ {
+ $this->content = $content;
+
+ extract($data, EXTR_OVERWRITE);
+ eval('?>' . $this->content);
+ }
+
+ /**
+ * 自动定位模板文件
+ * @access private
+ * @param string $template 模板文件规则
+ * @return string
+ */
+ private function parseTemplate($template)
+ {
+ if (empty($this->config['view_path'])) {
+ $this->config['view_path'] = $this->app->getModulePath() . 'view' . DIRECTORY_SEPARATOR;
+ }
+
+ $request = $this->app['request'];
+
+ // 获取视图根目录
+ if (strpos($template, '@')) {
+ // 跨模块调用
+ list($module, $template) = explode('@', $template);
+ }
+
+ if ($this->config['view_base']) {
+ // 基础视图目录
+ $module = isset($module) ? $module : $request->module();
+ $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : '');
+ } else {
+ $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path'];
+ }
+
+ $depr = $this->config['view_depr'];
+
+ if (0 !== strpos($template, '/')) {
+ $template = str_replace(['/', ':'], $depr, $template);
+ $controller = Loader::parseName($request->controller());
+
+ if ($controller) {
+ if ('' == $template) {
+ // 如果模板文件名为空 按照默认规则定位
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request);
+ } elseif (false === strpos($template, $depr)) {
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ }
+ }
+ } else {
+ $template = str_replace(['/', ':'], $depr, substr($template, 1));
+ }
+
+ return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ protected function getActionTemplate($request)
+ {
+ $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()];
+ $type = $this->config['auto_rule'];
+
+ return isset($rule[$type]) ? $rule[$type] : $rule[0];
+ }
+
+ /**
+ * 配置模板引擎
+ * @access private
+ * @param string|array $name 参数名
+ * @param mixed $value 参数值
+ * @return void
+ */
+ public function config($name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->config = array_merge($this->config, $name);
+ } elseif (is_null($value)) {
+ return isset($this->config[$name]) ? $this->config[$name] : null;
+ } else {
+ $this->config[$name] = $value;
+ }
+ }
+
+}
diff --git a/thinkphp/library/think/view/driver/Think.php b/thinkphp/library/think/view/driver/Think.php
new file mode 100644
index 00000000..9ea06826
--- /dev/null
+++ b/thinkphp/library/think/view/driver/Think.php
@@ -0,0 +1,187 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\view\driver;
+
+use think\App;
+use think\exception\TemplateNotFoundException;
+use think\Loader;
+use think\Template;
+
+class Think
+{
+ // 模板引擎实例
+ private $template;
+ private $app;
+
+ // 模板引擎参数
+ protected $config = [
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写
+ 'auto_rule' => 1,
+ // 视图基础目录(集中式)
+ 'view_base' => '',
+ // 模板起始路径
+ 'view_path' => '',
+ // 模板文件后缀
+ 'view_suffix' => 'html',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ // 是否开启模板编译缓存,设为false则每次都会重新编译
+ 'tpl_cache' => true,
+ ];
+
+ public function __construct(App $app, $config = [])
+ {
+ $this->app = $app;
+ $this->config = array_merge($this->config, (array) $config);
+
+ if (empty($this->config['view_path'])) {
+ $this->config['view_path'] = $app->getModulePath() . 'view' . DIRECTORY_SEPARATOR;
+ }
+
+ $this->template = new Template($app, $this->config);
+ }
+
+ /**
+ * 检测是否存在模板文件
+ * @access public
+ * @param string $template 模板文件或者模板规则
+ * @return bool
+ */
+ public function exists($template)
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ return is_file($template);
+ }
+
+ /**
+ * 渲染模板文件
+ * @access public
+ * @param string $template 模板文件
+ * @param array $data 模板变量
+ * @param array $config 模板参数
+ * @return void
+ */
+ public function fetch($template, $data = [], $config = [])
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ // 获取模板文件名
+ $template = $this->parseTemplate($template);
+ }
+
+ // 模板不存在 抛出异常
+ if (!is_file($template)) {
+ throw new TemplateNotFoundException('template not exists:' . $template, $template);
+ }
+
+ // 记录视图信息
+ $this->app
+ ->log('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]');
+
+ $this->template->fetch($template, $data, $config);
+ }
+
+ /**
+ * 渲染模板内容
+ * @access public
+ * @param string $template 模板内容
+ * @param array $data 模板变量
+ * @param array $config 模板参数
+ * @return void
+ */
+ public function display($template, $data = [], $config = [])
+ {
+ $this->template->display($template, $data, $config);
+ }
+
+ /**
+ * 自动定位模板文件
+ * @access private
+ * @param string $template 模板文件规则
+ * @return string
+ */
+ private function parseTemplate($template)
+ {
+ // 分析模板文件规则
+ $request = $this->app['request'];
+
+ // 获取视图根目录
+ if (strpos($template, '@')) {
+ // 跨模块调用
+ list($module, $template) = explode('@', $template);
+ }
+
+ if ($this->config['view_base']) {
+ // 基础视图目录
+ $module = isset($module) ? $module : $request->module();
+ $path = $this->config['view_base'] . ($module ? $module . DIRECTORY_SEPARATOR : '');
+ } else {
+ $path = isset($module) ? $this->app->getAppPath() . $module . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR : $this->config['view_path'];
+ }
+
+ $depr = $this->config['view_depr'];
+
+ if (0 !== strpos($template, '/')) {
+ $template = str_replace(['/', ':'], $depr, $template);
+ $controller = Loader::parseName($request->controller());
+
+ if ($controller) {
+ if ('' == $template) {
+ // 如果模板文件名为空 按照默认规则定位
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $this->getActionTemplate($request);
+ } elseif (false === strpos($template, $depr)) {
+ $template = str_replace('.', DIRECTORY_SEPARATOR, $controller) . $depr . $template;
+ }
+ }
+ } else {
+ $template = str_replace(['/', ':'], $depr, substr($template, 1));
+ }
+
+ return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
+ }
+
+ protected function getActionTemplate($request)
+ {
+ $rule = [$request->action(true), Loader::parseName($request->action(true)), $request->action()];
+ $type = $this->config['auto_rule'];
+
+ return isset($rule[$type]) ? $rule[$type] : $rule[0];
+ }
+
+ /**
+ * 配置或者获取模板引擎参数
+ * @access private
+ * @param string|array $name 参数名
+ * @param mixed $value 参数值
+ * @return mixed
+ */
+ public function config($name, $value = null)
+ {
+ if (is_array($name)) {
+ $this->template->config($name);
+ $this->config = array_merge($this->config, $name);
+ } elseif (is_null($value)) {
+ return $this->template->config($name);
+ } else {
+ $this->template->$name = $value;
+ $this->config[$name] = $value;
+ }
+ }
+
+ public function __call($method, $params)
+ {
+ return call_user_func_array([$this->template, $method], $params);
+ }
+}
diff --git a/thinkphp/library/traits/controller/Jump.php b/thinkphp/library/traits/controller/Jump.php
new file mode 100644
index 00000000..41f7e930
--- /dev/null
+++ b/thinkphp/library/traits/controller/Jump.php
@@ -0,0 +1,168 @@
+error();
+ * $this->redirect();
+ * }
+ * }
+ */
+namespace traits\controller;
+
+use think\Container;
+use think\exception\HttpResponseException;
+use think\Response;
+use think\response\Redirect;
+
+trait Jump
+{
+ /**
+ * 应用实例
+ * @var \think\App
+ */
+ protected $app;
+
+ /**
+ * 操作成功跳转的快捷方法
+ * @access protected
+ * @param mixed $msg 提示信息
+ * @param string $url 跳转的URL地址
+ * @param mixed $data 返回的数据
+ * @param integer $wait 跳转等待时间
+ * @param array $header 发送的Header信息
+ * @return void
+ */
+ protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
+ {
+ if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) {
+ $url = $_SERVER["HTTP_REFERER"];
+ } elseif ('' !== $url) {
+ $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Container::get('url')->build($url);
+ }
+
+ $result = [
+ 'code' => 1,
+ 'msg' => $msg,
+ 'data' => $data,
+ 'url' => $url,
+ 'wait' => $wait,
+ ];
+
+ $type = $this->getResponseType();
+ // 把跳转模板的渲染下沉,这样在 response_send 行为里通过getData()获得的数据是一致性的格式
+ if ('html' == strtolower($type)) {
+ $type = 'jump';
+ }
+
+ $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_success_tmpl')]);
+
+ throw new HttpResponseException($response);
+ }
+
+ /**
+ * 操作错误跳转的快捷方法
+ * @access protected
+ * @param mixed $msg 提示信息
+ * @param string $url 跳转的URL地址
+ * @param mixed $data 返回的数据
+ * @param integer $wait 跳转等待时间
+ * @param array $header 发送的Header信息
+ * @return void
+ */
+ protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
+ {
+ $type = $this->getResponseType();
+ if (is_null($url)) {
+ $url = $this->app['request']->isAjax() ? '' : 'javascript:history.back(-1);';
+ } elseif ('' !== $url) {
+ $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : $this->app['url']->build($url);
+ }
+
+ $result = [
+ 'code' => 0,
+ 'msg' => $msg,
+ 'data' => $data,
+ 'url' => $url,
+ 'wait' => $wait,
+ ];
+
+ if ('html' == strtolower($type)) {
+ $type = 'jump';
+ }
+
+ $response = Response::create($result, $type)->header($header)->options(['jump_template' => $this->app['config']->get('dispatch_error_tmpl')]);
+
+ throw new HttpResponseException($response);
+ }
+
+ /**
+ * 返回封装后的API数据到客户端
+ * @access protected
+ * @param mixed $data 要返回的数据
+ * @param integer $code 返回的code
+ * @param mixed $msg 提示信息
+ * @param string $type 返回数据格式
+ * @param array $header 发送的Header信息
+ * @return void
+ */
+ protected function result($data, $code = 0, $msg = '', $type = '', array $header = [])
+ {
+ $result = [
+ 'code' => $code,
+ 'msg' => $msg,
+ 'time' => time(),
+ 'data' => $data,
+ ];
+
+ $type = $type ?: $this->getResponseType();
+ $response = Response::create($result, $type)->header($header);
+
+ throw new HttpResponseException($response);
+ }
+
+ /**
+ * URL重定向
+ * @access protected
+ * @param string $url 跳转的URL表达式
+ * @param array|integer $params 其它URL参数
+ * @param integer $code http code
+ * @param array $with 隐式传参
+ * @return void
+ */
+ protected function redirect($url, $params = [], $code = 302, $with = [])
+ {
+ $response = new Redirect($url);
+
+ if (is_integer($params)) {
+ $code = $params;
+ $params = [];
+ }
+
+ $response->code($code)->params($params)->with($with);
+
+ throw new HttpResponseException($response);
+ }
+
+ /**
+ * 获取当前的response 输出类型
+ * @access protected
+ * @return string
+ */
+ protected function getResponseType()
+ {
+ if (!$this->app) {
+ $this->app = Container::get('app');
+ }
+
+ $isAjax = $this->app['request']->isAjax();
+ $config = $this->app['config'];
+
+ return $isAjax
+ ? $config->get('default_ajax_return')
+ : $config->get('default_return_type');
+ }
+}
diff --git a/thinkphp/logo.png b/thinkphp/logo.png
new file mode 100644
index 00000000..25fd0593
Binary files /dev/null and b/thinkphp/logo.png differ
diff --git a/thinkphp/phpunit.xml.dist b/thinkphp/phpunit.xml.dist
new file mode 100644
index 00000000..37c3d2b5
--- /dev/null
+++ b/thinkphp/phpunit.xml.dist
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ./library/think/*/tests/
+
+
+
+
+
+ ./library/
+
+ ./library/think/*/tests
+ ./library/think/*/assets
+ ./library/think/*/resources
+ ./library/think/*/vendor
+
+
+
+
\ No newline at end of file
diff --git a/thinkphp/tpl/default_index.tpl b/thinkphp/tpl/default_index.tpl
new file mode 100644
index 00000000..e5c1363a
--- /dev/null
+++ b/thinkphp/tpl/default_index.tpl
@@ -0,0 +1,10 @@
+*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px } :) ThinkPHP V5.112载初心不改(2006-2018) - 你值得信赖的PHP框架
';
+ }
+}
diff --git a/thinkphp/tpl/dispatch_jump.tpl b/thinkphp/tpl/dispatch_jump.tpl
new file mode 100644
index 00000000..583376bb
--- /dev/null
+++ b/thinkphp/tpl/dispatch_jump.tpl
@@ -0,0 +1,49 @@
+{__NOLAYOUT__}
+
+
+
+
+ 跳转提示
+
+
+
+
+
+
+
:)
+
+
+
+
:(
+
+
+
+
+
+ 页面自动 跳转 等待时间:
+
+
+
+
+
diff --git a/thinkphp/tpl/page_trace.tpl b/thinkphp/tpl/page_trace.tpl
new file mode 100644
index 00000000..2e5afbab
--- /dev/null
+++ b/thinkphp/tpl/page_trace.tpl
@@ -0,0 +1,71 @@
+
+
+
+ $value) {?>
+
+
+
+
+
+
+
+ $val) {
+ echo '' . (is_numeric($k) ? '' : $k.' : ') . htmlentities(print_r($val,true), ENT_COMPAT, 'utf-8') . ' ';
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
getUseTime().'s ';?>
+
+
+
+
diff --git a/thinkphp/tpl/think_exception.tpl b/thinkphp/tpl/think_exception.tpl
new file mode 100644
index 00000000..19ecbdc1
--- /dev/null
+++ b/thinkphp/tpl/think_exception.tpl
@@ -0,0 +1,507 @@
+'.end($names).'';
+ }
+ }
+
+ if(!function_exists('parse_file')){
+ function parse_file($file, $line)
+ {
+ return ''.basename($file)." line {$line}".' ';
+ }
+ }
+
+ if(!function_exists('parse_args')){
+ function parse_args($args)
+ {
+ $result = [];
+
+ foreach ($args as $key => $item) {
+ switch (true) {
+ case is_object($item):
+ $value = sprintf('object (%s)', parse_class(get_class($item)));
+ break;
+ case is_array($item):
+ if(count($item) > 3){
+ $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
+ } else {
+ $value = sprintf('[%s]', parse_args($item));
+ }
+ break;
+ case is_string($item):
+ if(strlen($item) > 20){
+ $value = sprintf(
+ '\'%s... \'',
+ htmlentities($item),
+ htmlentities(substr($item, 0, 20))
+ );
+ } else {
+ $value = sprintf("'%s'", htmlentities($item));
+ }
+ break;
+ case is_int($item):
+ case is_float($item):
+ $value = $item;
+ break;
+ case is_null($item):
+ $value = 'null ';
+ break;
+ case is_bool($item):
+ $value = '' . ($item ? 'true' : 'false') . ' ';
+ break;
+ case is_resource($item):
+ $value = 'resource ';
+ break;
+ default:
+ $value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
+ break;
+ }
+
+ $result[] = is_int($key) ? $value : "'{$key}' => {$value}";
+ }
+
+ return implode(', ', $result);
+ }
+ }
+?>
+
+
+
+
+ 系统发生错误
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Call Stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Exception Datas
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Environment Variables
+ $value) { ?>
+
+
+ empty
+
+
+
+ $val) { ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ThinkPHP
+
V
+
{ 十年磨一剑-为API开发设计的高性能框架 }
+
+
+
+
+
+
diff --git a/update.json b/update.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/update.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/vendor/autoload.php b/vendor/autoload.php
new file mode 100644
index 00000000..d224ed8b
--- /dev/null
+++ b/vendor/autoload.php
@@ -0,0 +1,7 @@
+